Browse Source

Merge remote-tracking branch 'upstream/master' into features/DevTools/PropertyType

pull/6280/head
Giuseppe Lippolis 5 years ago
parent
commit
9eb68d7be9
  1. 3
      build/MicroCom.targets
  2. 1
      native/Avalonia.Native/src/OSX/KeyTransform.mm
  3. 22
      native/Avalonia.Native/src/OSX/window.h
  4. 104
      native/Avalonia.Native/src/OSX/window.mm
  5. 2
      samples/ControlCatalog/Pages/DataGridPage.xaml
  6. 1
      samples/ControlCatalog/SideBar.xaml
  7. 1
      samples/RenderDemo/SideBar.xaml
  8. 4
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  9. 2
      src/Avalonia.Base/AvaloniaObject.cs
  10. 4
      src/Avalonia.Base/AvaloniaProperty.cs
  11. 4
      src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs
  12. 2
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  13. 6
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  14. 2
      src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
  15. 6
      src/Avalonia.Base/DirectPropertyBase.cs
  16. 2
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  17. 8
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  18. 60
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  19. 12
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  20. 2
      src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs
  21. 2
      src/Avalonia.Controls.DataGrid/DataGridClipboard.cs
  22. 2
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  23. 8
      src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs
  24. 6
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  25. 18
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  26. 2
      src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs
  27. 13
      src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs
  28. 6
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  29. 6
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  30. 22
      src/Avalonia.Controls/ApiCompatBaseline.txt
  31. 2
      src/Avalonia.Controls/AppBuilderBase.cs
  32. 4
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  33. 17
      src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs
  34. 9
      src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs
  35. 4
      src/Avalonia.Controls/AutoCompleteBox.cs
  36. 17
      src/Avalonia.Controls/ContextMenu.cs
  37. 10
      src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
  38. 4
      src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
  39. 2
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  40. 12
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  41. 8
      src/Avalonia.Controls/DefinitionBase.cs
  42. 23
      src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs
  43. 15
      src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs
  44. 4
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  45. 20
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  46. 9
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  47. 18
      src/Avalonia.Controls/Grid.cs
  48. 2
      src/Avalonia.Controls/ListBox.cs
  49. 2
      src/Avalonia.Controls/NativeMenuItemSeparator.cs
  50. 2
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  51. 3
      src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs
  52. 37
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  53. 4
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  54. 4
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  55. 6
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  56. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  57. 5
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  58. 16
      src/Avalonia.Controls/Primitives/Popup.cs
  59. 2
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  60. 9
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  61. 2
      src/Avalonia.Controls/Primitives/RangeBase.cs
  62. 34
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  63. 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  64. 40
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  65. 2
      src/Avalonia.Controls/Repeater/IElementFactory.cs
  66. 4
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  67. 2
      src/Avalonia.Controls/Repeater/ViewManager.cs
  68. 2
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  69. 2
      src/Avalonia.Controls/ScrollViewer.cs
  70. 2
      src/Avalonia.Controls/Selection/SelectionModel.cs
  71. 65
      src/Avalonia.Controls/TextBox.cs
  72. 8
      src/Avalonia.Controls/TickBar.cs
  73. 40
      src/Avalonia.Controls/ToolTip.cs
  74. 9
      src/Avalonia.Controls/TopLevel.cs
  75. 11
      src/Avalonia.Controls/Utils/UndoRedoHelper.cs
  76. 145
      src/Avalonia.Controls/Window.cs
  77. 34
      src/Avalonia.Controls/WindowBase.cs
  78. 8
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  79. 8
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  80. 8
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  81. 33
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  82. 123
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs
  83. 56
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  84. 165
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  85. 2
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml
  86. 13
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  87. 3
      src/Avalonia.Input/Avalonia.Input.csproj
  88. 2
      src/Avalonia.Input/Cursor.cs
  89. 13
      src/Avalonia.Input/FocusManager.cs
  90. 2
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  91. 2
      src/Avalonia.Input/ICommandSource.cs
  92. 17
      src/Avalonia.Input/ICustomKeyboardNavigation.cs
  93. 31
      src/Avalonia.Input/InputElement.cs
  94. 33
      src/Avalonia.Input/KeyboardNavigation.cs
  95. 111
      src/Avalonia.Input/KeyboardNavigationHandler.cs
  96. 7
      src/Avalonia.Input/KeyboardNavigationMode.cs
  97. 734
      src/Avalonia.Input/Navigation/TabNavigation.cs
  98. 4
      src/Avalonia.Input/TappedEventArgs.cs
  99. 4
      src/Avalonia.Input/TextInput/ITextInputMethodClient.cs
  100. 2
      src/Avalonia.Interactivity/Interactive.cs

3
build/MicroCom.targets

@ -15,7 +15,8 @@
Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs" Inputs="@(AvnComIdl);$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/**/*.cs"
Outputs="%(AvnComIdl.OutputFile)"> Outputs="%(AvnComIdl.OutputFile)">
<Message Importance="high" Text="Generating file %(AvnComIdl.OutputFile) from @(AvnComIdl)" /> <Message Importance="high" Text="Generating file %(AvnComIdl.OutputFile) from @(AvnComIdl)" />
<Exec Command="dotnet $(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/bin/$(Configuration)/netcoreapp3.1/MicroComGenerator.dll -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)" LogStandardErrorAsError="true" /> <Exec Command="dotnet &quot;$(MSBuildThisFileDirectory)../src/tools/MicroComGenerator/bin/$(Configuration)/netcoreapp3.1/MicroComGenerator.dll&quot; -i @(AvnComIdl) --cs %(AvnComIdl.OutputFile)"
LogStandardErrorAsError="true" />
<ItemGroup> <ItemGroup>
<!-- Remove and re-add generated file, this is needed for the clean build --> <!-- Remove and re-add generated file, this is needed for the clean build -->
<Compile Remove="%(AvnComIdl.OutputFile)"/> <Compile Remove="%(AvnComIdl.OutputFile)"/>

1
native/Avalonia.Native/src/OSX/KeyTransform.mm

@ -222,6 +222,7 @@ std::map<int, const char*> s_QwertyKeyMap =
{ 45, "n" }, { 45, "n" },
{ 46, "m" }, { 46, "m" },
{ 47, "." }, { 47, "." },
{ 48, "\t" },
{ 49, " " }, { 49, " " },
{ 50, "`" }, { 50, "`" },
{ 51, "" }, { 51, "" },

22
native/Avalonia.Native/src/OSX/window.h

@ -10,6 +10,8 @@ class WindowBaseImpl;
-(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose; -(void) setSwRenderedFrame: (AvnFramebuffer* _Nonnull) fb dispose: (IUnknown* _Nonnull) dispose;
-(void) onClosed; -(void) onClosed;
-(AvnPixelSize) getPixelSize; -(AvnPixelSize) getPixelSize;
-(AvnPlatformResizeReason) getResizeReason;
-(void) setResizeReason:(AvnPlatformResizeReason)reason;
@end @end
@interface AutoFitContentView : NSView @interface AutoFitContentView : NSView
@ -34,6 +36,7 @@ class WindowBaseImpl;
-(double) getScaling; -(double) getScaling;
-(double) getExtendedTitleBarHeight; -(double) getExtendedTitleBarHeight;
-(void) setIsExtended:(bool)value; -(void) setIsExtended:(bool)value;
-(bool) isDialog;
@end @end
struct INSWindowHolder struct INSWindowHolder
@ -50,4 +53,23 @@ struct IWindowStateChanged
virtual AvnWindowState WindowState () = 0; virtual AvnWindowState WindowState () = 0;
}; };
class ResizeScope
{
public:
ResizeScope(AvnView* _Nonnull view, AvnPlatformResizeReason reason)
{
_view = view;
_restore = [view getResizeReason];
[view setResizeReason:reason];
}
~ResizeScope()
{
[_view setResizeReason:_restore];
}
private:
AvnView* _Nonnull _view;
AvnPlatformResizeReason _restore;
};
#endif /* window_h */ #endif /* window_h */

104
native/Avalonia.Native/src/OSX/window.mm

@ -52,6 +52,7 @@ public:
[Window setBackingType:NSBackingStoreBuffered]; [Window setBackingType:NSBackingStoreBuffered];
[Window setOpaque:false]; [Window setOpaque:false];
[Window setContentView: StandardContainer];
} }
virtual HRESULT ObtainNSWindowHandle(void** ret) override virtual HRESULT ObtainNSWindowHandle(void** ret) override
@ -115,7 +116,7 @@ public:
return Window; return Window;
} }
virtual HRESULT Show(bool activate) override virtual HRESULT Show(bool activate, bool isDialog) override
{ {
START_COM_CALL; START_COM_CALL;
@ -124,7 +125,6 @@ public:
SetPosition(lastPositionSet); SetPosition(lastPositionSet);
UpdateStyle(); UpdateStyle();
[Window setContentView: StandardContainer];
[Window setTitle:_lastTitle]; [Window setTitle:_lastTitle];
if(ShouldTakeFocusOnShow() && activate) if(ShouldTakeFocusOnShow() && activate)
@ -277,7 +277,7 @@ public:
} }
} }
virtual HRESULT Resize(double x, double y) override virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override
{ {
if(_inResize) if(_inResize)
{ {
@ -287,6 +287,7 @@ public:
_inResize = true; _inResize = true;
START_COM_CALL; START_COM_CALL;
auto resizeBlock = ResizeScope(View, reason);
@autoreleasepool @autoreleasepool
{ {
@ -317,7 +318,7 @@ public:
{ {
if(!_shown) if(!_shown)
{ {
BaseEvents->Resized(AvnSize{x,y}); BaseEvents->Resized(AvnSize{x,y}, reason);
} }
[Window setContentSize:NSSize{x, y}]; [Window setContentSize:NSSize{x, y}];
@ -577,6 +578,11 @@ public:
return S_OK; return S_OK;
} }
virtual bool IsDialog()
{
return false;
}
protected: protected:
virtual NSWindowStyleMask GetStyle() virtual NSWindowStyleMask GetStyle()
{ {
@ -607,6 +613,7 @@ private:
NSRect _preZoomSize; NSRect _preZoomSize;
bool _transitioningWindowState; bool _transitioningWindowState;
bool _isClientAreaExtended; bool _isClientAreaExtended;
bool _isDialog;
AvnExtendClientAreaChromeHints _extendClientHints; AvnExtendClientAreaChromeHints _extendClientHints;
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
@ -667,13 +674,14 @@ private:
} }
} }
virtual HRESULT Show (bool activate) override virtual HRESULT Show (bool activate, bool isDialog) override
{ {
START_COM_CALL; START_COM_CALL;
@autoreleasepool @autoreleasepool
{ {
WindowBaseImpl::Show(activate); _isDialog = isDialog;
WindowBaseImpl::Show(activate, isDialog);
HideOrShowTrafficLights(); HideOrShowTrafficLights();
@ -705,6 +713,12 @@ private:
if(cparent == nullptr) if(cparent == nullptr)
return E_INVALIDARG; return E_INVALIDARG;
// If one tries to show a child window with a minimized parent window, then the parent window will be
// restored but MacOS isn't kind enough to *tell* us that, so the window will be left in a non-interactive
// state. Detect this and explicitly restore the parent window ourselves to avoid this situation.
if (cparent->WindowState() == Minimized)
cparent->SetWindowState(Normal);
[cparent->Window addChildWindow:Window ordered:NSWindowAbove]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
UpdateStyle(); UpdateStyle();
@ -1196,6 +1210,7 @@ private:
} }
_actualWindowState = _lastWindowState; _actualWindowState = _lastWindowState;
WindowEvents->WindowStateChanged(_actualWindowState);
} }
@ -1213,6 +1228,11 @@ private:
} }
} }
virtual bool IsDialog() override
{
return _isDialog;
}
protected: protected:
virtual NSWindowStyleMask GetStyle() override virtual NSWindowStyleMask GetStyle() override
{ {
@ -1373,6 +1393,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
bool _lastKeyHandled; bool _lastKeyHandled;
AvnPixelSize _lastPixelSize; AvnPixelSize _lastPixelSize;
NSObject<IRenderTarget>* _renderTarget; NSObject<IRenderTarget>* _renderTarget;
AvnPlatformResizeReason _resizeReason;
} }
- (void)onClosed - (void)onClosed
@ -1484,7 +1505,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_lastPixelSize.Height = (int)fsize.height; _lastPixelSize.Height = (int)fsize.height;
[self updateRenderTarget]; [self updateRenderTarget];
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
_parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
} }
} }
@ -1983,6 +2005,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
} }
- (AvnPlatformResizeReason)getResizeReason
{
return _resizeReason;
}
- (void)setResizeReason:(AvnPlatformResizeReason)reason
{
_resizeReason = reason;
}
@end @end
@ -2002,6 +2034,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_isExtended = value; _isExtended = value;
} }
-(bool) isDialog
{
return _parent->IsDialog();
}
-(double) getScaling -(double) getScaling
{ {
return _lastScaling; return _lastScaling;
@ -2180,7 +2217,22 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
-(BOOL)canBecomeKeyWindow -(BOOL)canBecomeKeyWindow
{ {
return _canBecomeKeyAndMain; if (_canBecomeKeyAndMain)
{
// If the window has a child window being shown as a dialog then don't allow it to become the key window.
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
if (ch.isDialog)
return false;
}
return true;
}
return false;
} }
-(BOOL)canBecomeMainWindow -(BOOL)canBecomeMainWindow
@ -2188,22 +2240,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return _canBecomeKeyAndMain; return _canBecomeKeyAndMain;
} }
-(bool) activateAppropriateChild: (bool)activating
{
for(NSWindow* uch in [self childWindows])
{
auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil)
continue;
[ch activateAppropriateChild:false];
return FALSE;
}
if(!activating)
[self makeKeyAndOrderFront:self];
return TRUE;
}
-(bool)shouldTryToHandleEvents -(bool)shouldTryToHandleEvents
{ {
return _isEnabled; return _isEnabled;
@ -2214,26 +2250,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_isEnabled = enable; _isEnabled = enable;
} }
-(void)makeKeyWindow
{
if([self activateAppropriateChild: true])
{
[super makeKeyWindow];
}
}
-(void)becomeKeyWindow -(void)becomeKeyWindow
{ {
[self showWindowMenuWithAppMenu]; [self showWindowMenuWithAppMenu];
if([self activateAppropriateChild: true]) if(_parent != nullptr)
{ {
if(_parent != nullptr) _parent->BaseEvents->Activated();
{
_parent->BaseEvents->Activated();
}
} }
[super becomeKeyWindow]; [super becomeKeyWindow];
} }
@ -2243,7 +2268,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
if(parent != nil) if(parent != nil)
{ {
[parent removeChildWindow:self]; [parent removeChildWindow:self];
[parent activateAppropriateChild: false];
} }
} }
@ -2378,7 +2402,7 @@ protected:
return NSWindowStyleMaskBorderless; return NSWindowStyleMaskBorderless;
} }
virtual HRESULT Resize(double x, double y) override virtual HRESULT Resize(double x, double y, AvnPlatformResizeReason reason) override
{ {
START_COM_CALL; START_COM_CALL;

2
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -28,7 +28,7 @@
DockPanel.Dock="Top"/> DockPanel.Dock="Top"/>
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All"> <DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" /> <DataGridTextColumn Header="Country" Binding="{Binding Name}" Width="6*" MinWidth="400" />
<!-- CompiledBinding example of usage. --> <!-- CompiledBinding example of usage. -->
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" /> <DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" /> <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" />

1
samples/ControlCatalog/SideBar.xaml

@ -16,7 +16,6 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border <Border
Margin="{TemplateBinding Margin}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel> <DockPanel>

1
samples/RenderDemo/SideBar.xaml

@ -7,7 +7,6 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Border <Border
Margin="{TemplateBinding Margin}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel> <DockPanel>

4
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -67,7 +67,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size, PlatformResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; } public Action<double> ScalingChanged { get; set; }
@ -134,7 +134,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
protected virtual void OnResized(Size size) protected virtual void OnResized(Size size)
{ {
Resized?.Invoke(size); Resized?.Invoke(size, PlatformResizeReason.Unspecified);
} }
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo

2
src/Avalonia.Base/AvaloniaObject.cs

@ -861,7 +861,7 @@ namespace Avalonia
} }
/// <summary> /// <summary>
/// Logs a mesage if the notification represents a binding error. /// Logs a message if the notification represents a binding error.
/// </summary> /// </summary>
/// <param name="property">The property being bound.</param> /// <param name="property">The property being bound.</param>
/// <param name="value">The binding notification.</param> /// <param name="value">The binding notification.</param>

4
src/Avalonia.Base/AvaloniaProperty.cs

@ -465,9 +465,9 @@ namespace Avalonia
/// Uses the visitor pattern to resolve an untyped property to a typed property. /// Uses the visitor pattern to resolve an untyped property to a typed property.
/// </summary> /// </summary>
/// <typeparam name="TData">The type of user data passed.</typeparam> /// <typeparam name="TData">The type of user data passed.</typeparam>
/// <param name="vistor">The visitor which will accept the typed property.</param> /// <param name="visitor">The visitor which will accept the typed property.</param>
/// <param name="data">The user data to pass.</param> /// <param name="data">The user data to pass.</param>
public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data) public abstract void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
where TData : struct; where TData : struct;
/// <summary> /// <summary>

4
src/Avalonia.Base/AvaloniaPropertyChangedEventArgs.cs

@ -58,8 +58,8 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// This will usually be true, except in /// This will usually be true, except in
/// <see cref="AvaloniaObject.OnPropertyChangedCore{T}(AvaloniaPropertyChangedEventArgs{T})"/> /// <see cref="AvaloniaObject.OnPropertyChangedCore{T}(AvaloniaPropertyChangedEventArgs{T})"/>
/// which recieves notifications for all changes to property values, whether a value with a higher /// which receives notifications for all changes to property values, whether a value with a higher
/// priority is present or not. When this property is false, the change that is being signalled /// priority is present or not. When this property is false, the change that is being signaled
/// has not resulted in a change to the property value on the object. /// has not resulted in a change to the property value on the object.
/// </remarks> /// </remarks>
public bool IsEffectiveValueChange { get; private set; } public bool IsEffectiveValueChange { get; private set; }

2
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@ -1271,7 +1271,7 @@ namespace Avalonia.Collections.Pooled
/// Reverses the elements in a range of this list. Following a call to this /// Reverses the elements in a range of this list. Following a call to this
/// method, an element in the range given by index and count /// method, an element in the range given by index and count
/// which was previously located at index i will now be located at /// which was previously located at index i will now be located at
/// index index + (index + count - i - 1). /// index + (index + count - i - 1).
/// </summary> /// </summary>
public void Reverse(int index, int count) public void Reverse(int index, int count)
{ {

6
src/Avalonia.Base/Data/Converters/BoolConverters.cs

@ -18,5 +18,11 @@ namespace Avalonia.Data.Converters
/// </summary> /// </summary>
public static readonly IMultiValueConverter Or = public static readonly IMultiValueConverter Or =
new FuncMultiValueConverter<bool, bool>(x => x.Any(y => y)); new FuncMultiValueConverter<bool, bool>(x => x.Any(y => y));
/// <summary>
/// A value converter that returns true when input is false and false when input is true.
/// </summary>
public static readonly IValueConverter Not =
new FuncValueConverter<bool, bool>(x => !x);
} }
} }

2
src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs

@ -20,7 +20,7 @@ namespace Avalonia.Data.Core.Plugins
/// </summary> /// </summary>
/// <param name="reference">A weak reference to the object.</param> /// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param> /// <param name="propertyName">The property name.</param>
/// <param name="inner">The inner property accessor used to aceess the property.</param> /// <param name="inner">The inner property accessor used to access the property.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made. /// property will be made.

6
src/Avalonia.Base/DirectPropertyBase.cs

@ -13,7 +13,7 @@ namespace Avalonia
/// <typeparam name="TValue">The type of the property's value.</typeparam> /// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <remarks> /// <remarks>
/// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base /// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base
/// class provides a non-owner-typed interface to a direct poperty. /// class provides a non-owner-typed interface to a direct property.
/// </remarks> /// </remarks>
public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue> public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue>
{ {
@ -123,9 +123,9 @@ namespace Avalonia
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> vistor, ref TData data) public override void Accept<TData>(IAvaloniaPropertyVisitor<TData> visitor, ref TData data)
{ {
vistor.Visit(this, ref data); visitor.Visit(this, ref data);
} }
/// <inheritdoc/> /// <inheritdoc/>

2
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@ -38,7 +38,7 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// Data validation is validation performed at the target of a binding, for example in a /// Data validation is validation performed at the target of a binding, for example in a
/// view model using the INotifyDataErrorInfo interface. Only certain properties on a /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
/// control (such as a TextBox's Text property) will be interested in recieving data /// control (such as a TextBox's Text property) will be interested in receiving data
/// validation messages so this feature must be explicitly enabled by setting this flag. /// validation messages so this feature must be explicitly enabled by setting this flag.
/// </remarks> /// </remarks>
public bool? EnableDataValidation { get; private set; } public bool? EnableDataValidation { get; private set; }

8
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@ -877,7 +877,7 @@ namespace Avalonia.Collections
if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred)) if (!CheckFlag(CollectionViewFlags.IsMoveToPageDeferred))
{ {
// if the temporaryGroup was not created yet and is out of sync // if the temporaryGroup was not created yet and is out of sync
// then create it so that we can use it as a refernce while paging. // then create it so that we can use it as a reference while paging.
if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count) if (IsGrouping && _temporaryGroup.ItemCount != InternalList.Count)
{ {
PrepareTemporaryGroups(); PrepareTemporaryGroups();
@ -889,7 +889,7 @@ namespace Avalonia.Collections
else if (IsGrouping) else if (IsGrouping)
{ {
// if the temporaryGroup was not created yet and is out of sync // if the temporaryGroup was not created yet and is out of sync
// then create it so that we can use it as a refernce while paging. // then create it so that we can use it as a reference while paging.
if (_temporaryGroup.ItemCount != InternalList.Count) if (_temporaryGroup.ItemCount != InternalList.Count)
{ {
// update the groups that get created for the // update the groups that get created for the
@ -1951,7 +1951,7 @@ namespace Avalonia.Collections
EnsureCollectionInSync(); EnsureCollectionInSync();
VerifyRefreshNotDeferred(); VerifyRefreshNotDeferred();
// for indicies larger than the count // for indices larger than the count
if (index >= Count || index < 0) if (index >= Count || index < 0)
{ {
throw new ArgumentOutOfRangeException("index"); throw new ArgumentOutOfRangeException("index");
@ -3800,7 +3800,7 @@ namespace Avalonia.Collections
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This method can be called from a constructor - it does not call /// This method can be called from a constructor - it does not call
/// any virtuals. The 'count' parameter is substitute for the real Count, /// any virtuals. The 'count' parameter is substitute for the real Count,
/// used only when newItem is null. /// used only when newItem is null.
/// In that case, this method sets IsCurrentAfterLast to true if and only /// In that case, this method sets IsCurrentAfterLast to true if and only
/// if newPosition >= count. This distinguishes between a null belonging /// if newPosition >= count. This distinguishes between a null belonging

60
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -25,6 +25,7 @@ using System.ComponentModel.DataAnnotations;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -67,7 +68,7 @@ namespace Avalonia.Controls
private const double DATAGRID_minimumColumnHeaderHeight = 4; private const double DATAGRID_minimumColumnHeaderHeight = 4;
internal const double DATAGRID_maximumStarColumnWidth = 10000; internal const double DATAGRID_maximumStarColumnWidth = 10000;
internal const double DATAGRID_minimumStarColumnWidth = 0.001; internal const double DATAGRID_minimumStarColumnWidth = 0.001;
private const double DATAGRID_mouseWheelDelta = 72.0; private const double DATAGRID_mouseWheelDelta = 50.0;
private const double DATAGRID_maxHeadersThickness = 32768; private const double DATAGRID_maxHeadersThickness = 32768;
private const double DATAGRID_defaultRowHeight = 22; private const double DATAGRID_defaultRowHeight = 22;
@ -2214,32 +2215,71 @@ namespace Avalonia.Controls
/// <param name="e">PointerWheelEventArgs</param> /// <param name="e">PointerWheelEventArgs</param>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e) protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{ {
if (IsEnabled && !e.Handled && DisplayData.NumDisplayedScrollingElements > 0) e.Handled = e.Handled || UpdateScroll(e.Delta * DATAGRID_mouseWheelDelta);
}
internal bool UpdateScroll(Vector delta)
{
if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0)
{ {
double scrollHeight = 0; var handled = false;
if (e.Delta.Y > 0) var scrollHeight = 0d;
// Vertical scroll handling
if (delta.Y > 0)
{ {
scrollHeight = Math.Max(-_verticalOffset, -DATAGRID_mouseWheelDelta); scrollHeight = Math.Max(-_verticalOffset, -delta.Y);
} }
else if (e.Delta.Y < 0) else if (delta.Y < 0)
{ {
if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible) if (_vScrollBar != null && VerticalScrollBarVisibility == ScrollBarVisibility.Visible)
{ {
scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), DATAGRID_mouseWheelDelta); scrollHeight = Math.Min(Math.Max(0, _vScrollBar.Maximum - _verticalOffset), -delta.Y);
} }
else else
{ {
double maximum = EdgedRowsHeightCalculated - CellsHeight; double maximum = EdgedRowsHeightCalculated - CellsHeight;
scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), DATAGRID_mouseWheelDelta); scrollHeight = Math.Min(Math.Max(0, maximum - _verticalOffset), -delta.Y);
} }
} }
if (scrollHeight != 0) if (scrollHeight != 0)
{ {
DisplayData.PendingVerticalScrollHeight = scrollHeight; DisplayData.PendingVerticalScrollHeight = scrollHeight;
handled = true;
}
// Horizontal scroll handling
if (delta.X != 0)
{
var originalHorizontalOffset = HorizontalOffset;
var horizontalOffset = originalHorizontalOffset - delta.X;
var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
if (horizontalOffset < 0)
{
horizontalOffset = 0;
}
if (horizontalOffset > widthNotVisible)
{
horizontalOffset = widthNotVisible;
}
if (horizontalOffset != originalHorizontalOffset)
{
HorizontalOffset = horizontalOffset;
handled = true;
}
}
if (handled)
{
InvalidateRowsMeasure(invalidateIndividualElements: false); InvalidateRowsMeasure(invalidateIndividualElements: false);
e.Handled = true; return true;
} }
} }
return false;
} }
/// <summary> /// <summary>
@ -5731,7 +5771,7 @@ namespace Avalonia.Controls
{ {
if (SelectionMode == DataGridSelectionMode.Single || !ctrl) if (SelectionMode == DataGridSelectionMode.Single || !ctrl)
{ {
// Unselect the currectly selected rows except the new selected row // Unselect the currently selected rows except the new selected row
action = DataGridSelectionAction.SelectCurrent; action = DataGridSelectionAction.SelectCurrent;
} }
else else

12
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -173,7 +173,15 @@ namespace Avalonia.Controls
} }
if (OwningRow != null) if (OwningRow != null)
{ {
e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled); var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
// Do not handle PointerPressed with touch,
// so we can start scroll gesture on the same event.
if (e.Pointer.Type != PointerType.Touch)
{
e.Handled = handled;
}
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true; OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
} }
} }
@ -197,7 +205,7 @@ namespace Avalonia.Controls
} }
// Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the // Makes sure the right gridline has the proper stroke and visibility. If lastVisibleColumn is specified, the
// right gridline will be collapsed if this cell belongs to the lastVisibileColumn and there is no filler column // right gridline will be collapsed if this cell belongs to the lastVisibleColumn and there is no filler column
internal void EnsureGridLine(DataGridColumn lastVisibleColumn) internal void EnsureGridLine(DataGridColumn lastVisibleColumn)
{ {
if (OwningGrid != null && _rightGridLine != null) if (OwningGrid != null && _rightGridLine != null)

2
src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs

@ -40,7 +40,7 @@ namespace Avalonia.Controls
return false; return false;
} }
// There is build warning if this is missiing // There is build warning if this is missing
public override int GetHashCode() public override int GetHashCode()
{ {
return base.GetHashCode(); return base.GetHashCode();

2
src/Avalonia.Controls.DataGrid/DataGridClipboard.cs

@ -189,7 +189,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// DataGrid row item used for proparing the ClipboardRowContent. /// DataGrid row item used for preparing the ClipboardRowContent.
/// </summary> /// </summary>
public object Item public object Item
{ {

2
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -795,7 +795,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// If the DataGrid is using using layout rounding, the pixel snapping will force all widths to /// If the DataGrid is using layout rounding, the pixel snapping will force all widths to
/// whole numbers. Since the column widths aren't visual elements, they don't go through the normal /// whole numbers. Since the column widths aren't visual elements, they don't go through the normal
/// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some /// rounding process, so we need to do it ourselves. If we don't, then we'll end up with some
/// pixel gaps and/or overlaps between columns. /// pixel gaps and/or overlaps between columns.

8
src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs

@ -79,7 +79,7 @@ namespace Avalonia.Controls
set; set;
} }
internal void AddRecylableRow(DataGridRow row) internal void AddRecyclableRow(DataGridRow row)
{ {
Debug.Assert(!_recyclableRows.Contains(row)); Debug.Assert(!_recyclableRows.Contains(row));
row.DetachFromDataGrid(true); row.DetachFromDataGrid(true);
@ -120,7 +120,7 @@ namespace Avalonia.Controls
{ {
if (row.IsRecyclable) if (row.IsRecyclable)
{ {
AddRecylableRow(row); AddRecyclableRow(row);
} }
else else
{ {
@ -193,7 +193,7 @@ namespace Avalonia.Controls
internal void FullyRecycleElements() internal void FullyRecycleElements()
{ {
// Fully recycle Recycleable rows and transfer them to Recycled rows // Fully recycle Recyclable rows and transfer them to Recycled rows
while (_recyclableRows.Count > 0) while (_recyclableRows.Count > 0)
{ {
DataGridRow row = _recyclableRows.Pop(); DataGridRow row = _recyclableRows.Pop();
@ -202,7 +202,7 @@ namespace Avalonia.Controls
Debug.Assert(!_fullyRecycledRows.Contains(row)); Debug.Assert(!_fullyRecycledRows.Contains(row));
_fullyRecycledRows.Push(row); _fullyRecycledRows.Push(row);
} }
// Fully recycle Recycleable GroupHeaders and transfer them to Recycled GroupHeaders // Fully recycle Recyclable GroupHeaders and transfer them to Recycled GroupHeaders
while (_recyclableGroupHeaders.Count > 0) while (_recyclableGroupHeaders.Count > 0)
{ {
DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop(); DataGridRowGroupHeader groupHeader = _recyclableGroupHeaders.Pop();

6
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -392,7 +392,7 @@ namespace Avalonia.Controls
set; set;
} }
// Height that the row will eventually end up at after a possible detalis animation has completed // Height that the row will eventually end up at after a possible details animation has completed
internal double TargetHeight internal double TargetHeight
{ {
get get
@ -517,7 +517,7 @@ namespace Avalonia.Controls
return base.MeasureOverride(availableSize); return base.MeasureOverride(availableSize);
} }
//Allow the DataGrid specific componets to adjust themselves based on new values //Allow the DataGrid specific components to adjust themselves based on new values
if (_headerElement != null) if (_headerElement != null)
{ {
_headerElement.InvalidateMeasure(); _headerElement.InvalidateMeasure();
@ -722,7 +722,7 @@ namespace Avalonia.Controls
if (_bottomGridLine != null) if (_bottomGridLine != null)
{ {
// It looks like setting Visibility sometimes has side effects so make sure the value is actually // It looks like setting Visibility sometimes has side effects so make sure the value is actually
// diffferent before setting it // different before setting it
bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All; bool newVisibility = OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.Horizontal || OwningGrid.GridLinesVisibility == DataGridGridLinesVisibility.All;
if (newVisibility != _bottomGridLine.IsVisible) if (newVisibility != _bottomGridLine.IsVisible)

18
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -1193,7 +1193,7 @@ namespace Avalonia.Controls
else else
{ {
groupHeader = element as DataGridRowGroupHeader; groupHeader = element as DataGridRowGroupHeader;
Debug.Assert(groupHeader != null); // Nothig other and Rows and RowGroups now Debug.Assert(groupHeader != null); // Nothing other and Rows and RowGroups now
if (groupHeader != null) if (groupHeader != null)
{ {
groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1]; groupHeader.TotalIndent = (groupHeader.Level == 0) ? 0 : RowGroupSublevelIndents[groupHeader.Level - 1];
@ -1636,7 +1636,7 @@ namespace Avalonia.Controls
if (slot >= DisplayData.FirstScrollingSlot && if (slot >= DisplayData.FirstScrollingSlot &&
slot <= DisplayData.LastScrollingSlot) slot <= DisplayData.LastScrollingSlot)
{ {
// Additional row takes the spot of a displayed row - it is necessarilly displayed // Additional row takes the spot of a displayed row - it is necessarily displayed
return true; return true;
} }
else if (DisplayData.FirstScrollingSlot == -1 && else if (DisplayData.FirstScrollingSlot == -1 &&
@ -1825,7 +1825,7 @@ namespace Avalonia.Controls
if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset))
{ {
// We've scrolled off more of the first row than what's possible. This can happen // We've scrolled off more of the first row than what's possible. This can happen
// if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling // if the first row got shorter (Ex: Collapsing RowDetails) or if the user has a recycling
// cleanup issue. In this case, simply try to display the next row as the first row instead // cleanup issue. In this case, simply try to display the next row as the first row instead
if (newFirstScrollingSlot < SlotCount - 1) if (newFirstScrollingSlot < SlotCount - 1)
{ {
@ -2014,7 +2014,7 @@ namespace Avalonia.Controls
if (recycleRow) if (recycleRow)
{ {
DisplayData.AddRecylableRow(dataGridRow); DisplayData.AddRecyclableRow(dataGridRow);
} }
else else
{ {
@ -2265,7 +2265,7 @@ namespace Avalonia.Controls
if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1) if (parentGroupInfo.LastSubItemSlot - parentGroupInfo.Slot == 1)
{ {
// We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed // We just added the first item to a RowGroup so the header should transition from Empty to either Expanded or Collapsed
EnsureAnscestorsExpanderButtonChecked(parentGroupInfo); EnsureAncestorsExpanderButtonChecked(parentGroupInfo);
} }
} }
} }
@ -2407,7 +2407,7 @@ namespace Avalonia.Controls
return treeCount; return treeCount;
} }
private void EnsureAnscestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo) private void EnsureAncestorsExpanderButtonChecked(DataGridRowGroupInfo parentGroupInfo)
{ {
if (IsSlotVisible(parentGroupInfo.Slot)) if (IsSlotVisible(parentGroupInfo.Slot))
{ {
@ -2789,11 +2789,11 @@ namespace Avalonia.Controls
return null; return null;
} }
internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisibile, bool setCurrent) internal void OnRowGroupHeaderToggled(DataGridRowGroupHeader groupHeader, bool newIsVisible, bool setCurrent)
{ {
Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0); Debug.Assert(groupHeader.RowGroupInfo.CollectionViewGroup.ItemCount > 0);
if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisibile, setCurrent); }) || !CommitEdit()) if (WaitForLostFocus(delegate { OnRowGroupHeaderToggled(groupHeader, newIsVisible, setCurrent); }) || !CommitEdit())
{ {
return; return;
} }
@ -2804,7 +2804,7 @@ namespace Avalonia.Controls
UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false); UpdateSelectionAndCurrency(CurrentColumnIndex, groupHeader.RowGroupInfo.Slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
} }
UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisibile, isDisplayed: true); UpdateRowGroupVisibility(groupHeader.RowGroupInfo, newIsVisible, isDisplayed: true);
ComputeScrollBarsLayout(); ComputeScrollBarsLayout();
// We need force arrange since our Scrollings Rows could update without automatically triggering layout // We need force arrange since our Scrollings Rows could update without automatically triggering layout

2
src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs

@ -140,7 +140,7 @@ namespace Avalonia.Controls.Primitives
if (dataGridColumn.IsFrozen) if (dataGridColumn.IsFrozen)
{ {
columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height)); columnHeader.Arrange(new Rect(frozenLeftEdge, 0, dataGridColumn.LayoutRoundedWidth, finalSize.Height));
columnHeader.Clip = null; // The layout system could have clipped this becaues it's not aware of our render transform columnHeader.Clip = null; // The layout system could have clipped this because it's not aware of our render transform
if (DragColumn == dataGridColumn && DragIndicator != null) if (DragColumn == dataGridColumn && DragIndicator != null)
{ {
dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset; dragIndicatorLeftEdge = frozenLeftEdge + DragIndicatorOffset;

13
src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs

@ -5,6 +5,9 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
@ -16,6 +19,11 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public sealed class DataGridRowsPresenter : Panel public sealed class DataGridRowsPresenter : Panel
{ {
public DataGridRowsPresenter()
{
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
}
internal DataGrid OwningGrid internal DataGrid OwningGrid
{ {
get; get;
@ -176,6 +184,11 @@ namespace Avalonia.Controls.Primitives
return new Size(totalCellsWidth + headerWidth, totalHeight); return new Size(totalCellsWidth + headerWidth, totalHeight);
} }
private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
{
e.Handled = e.Handled || OwningGrid.UpdateScroll(-e.Delta);
}
#if DEBUG #if DEBUG
internal void PrintChildren() internal void PrintChildren()
{ {

6
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -247,7 +247,11 @@
<DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/> <DataGridColumnHeader Name="PART_TopRightCornerHeader" Grid.Column="2"/>
<Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/> <Rectangle Name="PART_ColumnHeadersAndRowsSeparator" Grid.ColumnSpan="3" VerticalAlignment="Bottom" StrokeThickness="1" Height="1" Fill="{DynamicResource ThemeControlMidHighBrush}"/>
<DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" /> <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1">
<DataGridRowsPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
</DataGridRowsPresenter.GestureRecognizers>
</DataGridRowsPresenter>
<Rectangle Name="PART_BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" /> <Rectangle Name="PART_BottomRightCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Column="2" Grid.Row="2" />
<Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" /> <Rectangle Name="BottomLeftCorner" Fill="{DynamicResource ThemeControlMidHighBrush}" Grid.Row="2" Grid.ColumnSpan="2" />
<ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/> <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}"/>

6
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -601,7 +601,11 @@
<DataGridRowsPresenter Name="PART_RowsPresenter" <DataGridRowsPresenter Name="PART_RowsPresenter"
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="2" Grid.RowSpan="2"
Grid.ColumnSpan="3" /> Grid.ColumnSpan="3">
<DataGridRowsPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="True" CanVerticallyScroll="True" />
</DataGridRowsPresenter.GestureRecognizers>
</DataGridRowsPresenter>
<Rectangle Name="PART_BottomRightCorner" <Rectangle Name="PART_BottomRightCorner"
Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}" Fill="{DynamicResource DataGridScrollBarsSeparatorBackground}"
Grid.Column="2" Grid.Column="2"

22
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -30,15 +30,29 @@ MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownV
MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.NumericUpDownValueChangedEventArgs.OldValue.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.StyledProperty<System.Boolean> Avalonia.StyledProperty<System.Boolean> Avalonia.Controls.ScrollViewer.AllowAutoHideProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.AvaloniaProperty<Avalonia.Media.Stretch> Avalonia.Controls.Viewbox.StretchProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<System.ComponentModel.CancelEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs> Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.ShutdownRequested' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<System.ComponentModel.CancelEventArgs>)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.add_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<System.ComponentModel.CancelEventArgs>)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime.remove_ShutdownRequested(System.EventHandler<Avalonia.Controls.ApplicationLifetimes.ShutdownRequestedEventArgs>)' is present in the implementation but not in the contract.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Action<Avalonia.Size, Avalonia.Platform.PlatformResizeReason> Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Action<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.Resized.get()' is present in the contract but not in the implementation.
MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.Resized.get()' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action<Avalonia.Size, Avalonia.Platform.PlatformResizeReason>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action<Avalonia.Size>)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
Total Issues: 42 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowBaseImpl.Show(System.Boolean, System.Boolean)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
Total Issues: 56

2
src/Avalonia.Controls/AppBuilderBase.cs

@ -273,7 +273,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Sets up the platform-speciic services for the <see cref="Application"/>. /// Sets up the platform-specific services for the <see cref="Application"/>.
/// </summary> /// </summary>
private void Setup() private void Setup()
{ {

4
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -48,7 +48,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup; public event EventHandler<ControlledApplicationLifetimeStartupEventArgs> Startup;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<CancelEventArgs> ShutdownRequested; public event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit; public event EventHandler<ControlledApplicationLifetimeExitEventArgs> Exit;
@ -134,7 +134,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
_activeLifetime = null; _activeLifetime = null;
} }
private void OnShutdownRequested(object sender, CancelEventArgs e) private void OnShutdownRequested(object sender, ShutdownRequestedEventArgs e)
{ {
ShutdownRequested?.Invoke(this, e); ShutdownRequested?.Invoke(this, e);

17
src/Avalonia.Controls/ApplicationLifetimes/IClassicDesktopStyleApplicationLifetime.cs

@ -37,14 +37,21 @@ namespace Avalonia.Controls.ApplicationLifetimes
IReadOnlyList<Window> Windows { get; } IReadOnlyList<Window> Windows { get; }
/// <summary> /// <summary>
/// Raised by the platform when a shutdown is requested. /// Raised by the platform when an application shutdown is requested.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. This event /// Application Shutdown can be requested for various reasons like OS shutdown.
/// provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application ///
/// On Windows this will be called when an OS Session (logout or shutdown) terminates. Cancelling the eventargs will
/// block OS shutdown.
///
/// On OSX this has the same behavior as on Windows and in addition:
/// This event is raised via the Quit menu or right-clicking on the application icon and selecting Quit.
///
/// This event provides a first-chance to cancel application shutdown; if shutdown is not canceled at this point the application
/// will try to close each non-owned open window, invoking the <see cref="Window.Closing"/> event on each and allowing /// will try to close each non-owned open window, invoking the <see cref="Window.Closing"/> event on each and allowing
/// each window to cancel the shutdown. /// each window to cancel the shutdown of the application. Windows cannot however prevent OS shutdown.
/// </remarks> /// </remarks>
event EventHandler<CancelEventArgs> ShutdownRequested; event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;
} }
} }

9
src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs

@ -0,0 +1,9 @@
using System.ComponentModel;
namespace Avalonia.Controls.ApplicationLifetimes
{
public class ShutdownRequestedEventArgs : CancelEventArgs
{
}
}

4
src/Avalonia.Controls/AutoCompleteBox.cs

@ -2005,7 +2005,7 @@ namespace Avalonia.Controls
// The TextBox.TextChanged event was not firing immediately and // The TextBox.TextChanged event was not firing immediately and
// was causing an immediate update, even with wrapping. If there is // was causing an immediate update, even with wrapping. If there is
// a selection currently, no update should happen. // a selection currently, no update should happen.
if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != TextBox.Text.Length) if (IsTextCompletionEnabled && TextBox != null && TextBoxSelectionLength > 0 && TextBoxSelectionStart != (TextBox.Text?.Length ?? 0))
{ {
return; return;
} }
@ -2303,7 +2303,7 @@ namespace Avalonia.Controls
{ {
if (IsTextCompletionEnabled && TextBox != null && userInitiated) if (IsTextCompletionEnabled && TextBox != null && userInitiated)
{ {
int currentLength = TextBox.Text.Length; int currentLength = TextBox.Text?.Length ?? 0;
int selectionStart = TextBoxSelectionStart; int selectionStart = TextBoxSelectionStart;
if (selectionStart == text.Length && selectionStart > _textSelectionStart) if (selectionStart == text.Length && selectionStart > _textSelectionStart)
{ {

17
src/Avalonia.Controls/ContextMenu.cs

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -21,7 +21,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control context menu. /// A control context menu.
/// </summary> /// </summary>
public class ContextMenu : MenuBase, ISetterValue public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider
{ {
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalOffset"/> property. /// Defines the <see cref="HorizontalOffset"/> property.
@ -82,6 +82,7 @@ namespace Avalonia.Controls
private Popup? _popup; private Popup? _popup;
private List<Control>? _attachedControls; private List<Control>? _attachedControls;
private IInputElement? _previousFocus; private IInputElement? _previousFocus;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class. /// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -304,6 +305,14 @@ namespace Avalonia.Controls
} }
} }
IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new MenuItemContainerGenerator(this); return new MenuItemContainerGenerator(this);
@ -364,6 +373,8 @@ namespace Avalonia.Controls
{ {
_previousFocus = FocusManager.Instance?.Current; _previousFocus = FocusManager.Instance?.Current;
Focus(); Focus();
_popupHostChangedHandler?.Invoke(_popup!.Host);
} }
private void PopupClosing(object sender, CancelEventArgs e) private void PopupClosing(object sender, CancelEventArgs e)
@ -397,6 +408,8 @@ namespace Avalonia.Controls
RoutedEvent = MenuClosedEvent, RoutedEvent = MenuClosedEvent,
Source = this, Source = this,
}); });
_popupHostChangedHandler?.Invoke(null);
} }
private void PopupKeyUp(object sender, KeyEventArgs e) private void PopupKeyUp(object sender, KeyEventArgs e)

10
src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs

@ -26,13 +26,13 @@ namespace Avalonia.Controls.Converters
Right ? Indent * scalarDepth : 0, Right ? Indent * scalarDepth : 0,
Bottom ? Indent * scalarDepth : 0); Bottom ? Indent * scalarDepth : 0);
} }
else if (value is Thickness thinknessDepth) else if (value is Thickness thicknessDepth)
{ {
return new Thickness( return new Thickness(
Left ? Indent * thinknessDepth.Left : 0, Left ? Indent * thicknessDepth.Left : 0,
Top ? Indent * thinknessDepth.Top : 0, Top ? Indent * thicknessDepth.Top : 0,
Right ? Indent * thinknessDepth.Right : 0, Right ? Indent * thicknessDepth.Right : 0,
Bottom ? Indent * thinknessDepth.Bottom : 0); Bottom ? Indent * thicknessDepth.Bottom : 0);
} }
return new Thickness(0); return new Thickness(0);

4
src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs

@ -16,7 +16,7 @@ namespace Avalonia.Controls.Converters
if (parameter == null || if (parameter == null ||
values == null || values == null ||
values.Count != 4 || values.Count != 4 ||
!(values[0] is ScrollBarVisibility visiblity) || !(values[0] is ScrollBarVisibility visibility) ||
!(values[1] is double offset) || !(values[1] is double offset) ||
!(values[2] is double extent) || !(values[2] is double extent) ||
!(values[3] is double viewport)) !(values[3] is double viewport))
@ -24,7 +24,7 @@ namespace Avalonia.Controls.Converters
return AvaloniaProperty.UnsetValue; return AvaloniaProperty.UnsetValue;
} }
if (visiblity == ScrollBarVisibility.Auto) if (visibility == ScrollBarVisibility.Auto)
{ {
if (extent == viewport) if (extent == viewport)
{ {

2
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -71,7 +71,7 @@ namespace Avalonia.Controls
x => x.MonthVisible, (x, v) => x.MonthVisible = v); x => x.MonthVisible, (x, v) => x.MonthVisible = v);
/// <summary> /// <summary>
/// Defiens the <see cref="YearFormat"/> Property /// Defines the <see cref="YearFormat"/> Property
/// </summary> /// </summary>
public static readonly DirectProperty<DatePicker, string> YearFormatProperty = public static readonly DirectProperty<DatePicker, string> YearFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>(nameof(YearFormat), AvaloniaProperty.RegisterDirect<DatePicker, string>(nameof(YearFormat),

12
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@ -220,15 +220,15 @@ namespace Avalonia.Controls.Primitives
if (dy > 0) // Scroll Down if (dy > 0) // Scroll Down
{ {
int numContsToMove = 0; int numCountsToMove = 0;
for (int i = 0; i < children.Count; i++) for (int i = 0; i < children.Count; i++)
{ {
if (children[i].Bounds.Bottom - dy < 0) if (children[i].Bounds.Bottom - dy < 0)
numContsToMove++; numCountsToMove++;
else else
break; break;
} }
children.MoveRange(0, numContsToMove, children.Count); children.MoveRange(0, numCountsToMove, children.Count);
var scrollHeight = _extent.Height - Viewport.Height; var scrollHeight = _extent.Height - Viewport.Height;
if (ShouldLoop && value.Y >= scrollHeight - _extentOne) if (ShouldLoop && value.Y >= scrollHeight - _extentOne)
@ -236,15 +236,15 @@ namespace Avalonia.Controls.Primitives
} }
else if (dy < 0) // Scroll Up else if (dy < 0) // Scroll Up
{ {
int numContsToMove = 0; int numCountsToMove = 0;
for (int i = children.Count - 1; i >= 0; i--) for (int i = children.Count - 1; i >= 0; i--)
{ {
if (children[i].Bounds.Top - dy > Bounds.Height) if (children[i].Bounds.Top - dy > Bounds.Height)
numContsToMove++; numCountsToMove++;
else else
break; break;
} }
children.MoveRange(children.Count - numContsToMove, numContsToMove, 0); children.MoveRange(children.Count - numCountsToMove, numCountsToMove, 0);
if (ShouldLoop && value.Y < _extentOne) if (ShouldLoop && value.Y < _extentOne)
_offset = new Vector(0, value.Y + (_extentOne * 50)); _offset = new Vector(0, value.Y + (_extentOne * 50));
} }

8
src/Avalonia.Controls/DefinitionBase.cs

@ -35,7 +35,7 @@ namespace Avalonia.Controls
if (_sharedState == null) if (_sharedState == null)
{ {
// start with getting SharedSizeGroup value. // start with getting SharedSizeGroup value.
// this property is NOT inhereted which should result in better overall perf. // this property is NOT inherited which should result in better overall perf.
string sharedSizeGroupId = SharedSizeGroup; string sharedSizeGroupId = SharedSizeGroup;
if (sharedSizeGroupId != null) if (sharedSizeGroupId != null)
{ {
@ -52,7 +52,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Callback to notify about exitting model tree. /// Callback to notify about exiting model tree.
/// </summary> /// </summary>
internal void OnExitParentTree() internal void OnExitParentTree()
{ {
@ -458,7 +458,7 @@ namespace Avalonia.Controls
private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content" private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content"
private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's
private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure private double _measureSize; // size, calculated to be the input constraint size for Child.Measure
private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations
private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case) private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case)
@ -556,7 +556,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Propogates invalidations for all registered definitions. /// Propagates invalidations for all registered definitions.
/// Resets its own state. /// Resets its own state.
/// </summary> /// </summary>
internal void Invalidate() internal void Invalidate()

23
src/Avalonia.Controls/Diagnostics/IPopupHostProvider.cs

@ -0,0 +1,23 @@
using System;
using Avalonia.Controls.Primitives;
#nullable enable
namespace Avalonia.Controls.Diagnostics
{
/// <summary>
/// Diagnostics interface to retrieve an associated <see cref="IPopupHost"/>.
/// </summary>
public interface IPopupHostProvider
{
/// <summary>
/// The popup host.
/// </summary>
IPopupHost? PopupHost { get; }
/// <summary>
/// Raised when the popup host changes.
/// </summary>
event Action<IPopupHost?>? PopupHostChanged;
}
}

15
src/Avalonia.Controls/Diagnostics/ToolTipDiagnostics.cs

@ -0,0 +1,15 @@
#nullable enable
namespace Avalonia.Controls.Diagnostics
{
/// <summary>
/// Helper class to provide diagnostics information for <see cref="ToolTip"/>.
/// </summary>
public static class ToolTipDiagnostics
{
/// <summary>
/// Provides access to the internal <see cref="ToolTip.ToolTipProperty"/> for use in DevTools.
/// </summary>
public static AvaloniaProperty<ToolTip?> ToolTipProperty = ToolTip.ToolTipProperty;
}
}

4
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -31,7 +31,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
set set
{ {
_clientSize = value; _clientSize = value;
Resized?.Invoke(value); Resized?.Invoke(value, PlatformResizeReason.Unspecified);
} }
} }
@ -49,7 +49,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
public Action<RawInputEventArgs> Input { get; set; } public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size, PlatformResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; } public Action<double> ScalingChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }

20
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -1,19 +1,18 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Rendering;
#nullable enable #nullable enable
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
public abstract class FlyoutBase : AvaloniaObject public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
{ {
static FlyoutBase() static FlyoutBase()
{ {
@ -59,6 +58,7 @@ namespace Avalonia.Controls.Primitives
private Rect? _enlargedPopupRect; private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect; private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable; private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
public FlyoutBase() public FlyoutBase()
{ {
@ -103,6 +103,14 @@ namespace Avalonia.Controls.Primitives
private set => SetAndRaise(TargetProperty, ref _target, value); private set => SetAndRaise(TargetProperty, ref _target, value);
} }
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler? Closed; public event EventHandler? Closed;
public event EventHandler<CancelEventArgs>? Closing; public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opened; public event EventHandler? Opened;
@ -363,6 +371,8 @@ namespace Avalonia.Controls.Primitives
private void OnPopupOpened(object sender, EventArgs e) private void OnPopupOpened(object sender, EventArgs e)
{ {
IsOpen = true; IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup!.Host);
} }
private void OnPopupClosing(object sender, CancelEventArgs e) private void OnPopupClosing(object sender, CancelEventArgs e)
@ -376,6 +386,8 @@ namespace Avalonia.Controls.Primitives
private void OnPopupClosed(object sender, EventArgs e) private void OnPopupClosed(object sender, EventArgs e)
{ {
HideCore(false); HideCore(false);
_popupHostChangedHandler?.Invoke(null);
} }
// This method is handling both popup logical tree and target logical tree. // This method is handling both popup logical tree and target logical tree.
@ -552,7 +564,7 @@ namespace Avalonia.Controls.Primitives
internal static void SetPresenterClasses(IControl presenter, Classes classes) internal static void SetPresenterClasses(IControl presenter, Classes classes)
{ {
//Remove any classes no longer in use, ignoring pseudoclasses //Remove any classes no longer in use, ignoring pseudo classes
for (int i = presenter.Classes.Count - 1; i >= 0; i--) for (int i = presenter.Classes.Count - 1; i >= 0; i--)
{ {
if (!classes.Contains(presenter.Classes[i]) && if (!classes.Contains(presenter.Classes[i]) &&

9
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@ -6,15 +6,6 @@ namespace Avalonia.Controls
{ {
public class FlyoutPresenter : ContentControl public class FlyoutPresenter : ContentControl
{ {
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
public CornerRadius CornerRadius
{
get => GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
if (e.Key == Key.Escape) if (e.Key == Key.Escape)

18
src/Avalonia.Controls/Grid.cs

@ -330,7 +330,7 @@ namespace Avalonia.Controls
// value of Auto column), "cell 2 1" needs to be calculated first, // value of Auto column), "cell 2 1" needs to be calculated first,
// as it contributes to the Auto column's calculated value. // as it contributes to the Auto column's calculated value.
// At the same time in order to accurately calculate constraint // At the same time in order to accurately calculate constraint
// height for "cell 2 1", "cell 1 2" needs to be calcualted first, // height for "cell 2 1", "cell 1 2" needs to be calculated first,
// as it contributes to Auto row height, which is used in the // as it contributes to Auto row height, which is used in the
// computation of Star row resolved height. // computation of Star row resolved height.
// //
@ -405,11 +405,11 @@ namespace Avalonia.Controls
// //
// where: // where:
// * all [Measure GroupN] - regular children measure process - // * all [Measure GroupN] - regular children measure process -
// each cell is measured given contraint size as an input // each cell is measured given constraint size as an input
// and each cell's desired size is accumulated on the // and each cell's desired size is accumulated on the
// corresponding column / row; // corresponding column / row;
// * [Measure Group2'] - is when each cell is measured with // * [Measure Group2'] - is when each cell is measured with
// infinit height as a constraint and a cell's desired // infinite height as a constraint and a cell's desired
// height is ignored; // height is ignored;
// * [Measure Groups''] - is when each cell is measured (second // * [Measure Groups''] - is when each cell is measured (second
// time during single Grid.MeasureOverride) regularly but its // time during single Grid.MeasureOverride) regularly but its
@ -780,7 +780,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Initializes DefinitionsU memeber either to user supplied ColumnDefinitions collection /// Initializes DefinitionsU member either to user supplied ColumnDefinitions collection
/// or to a default single element collection. DefinitionsU gets trimmed to size. /// or to a default single element collection. DefinitionsU gets trimmed to size.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
@ -821,7 +821,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Initializes DefinitionsV memeber either to user supplied RowDefinitions collection /// Initializes DefinitionsV member either to user supplied RowDefinitions collection
/// or to a default single element collection. DefinitionsV gets trimmed to size. /// or to a default single element collection. DefinitionsV gets trimmed to size.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
@ -2132,7 +2132,7 @@ namespace Avalonia.Controls
// //
// Fortunately, our scenarios tend to have a small number of columns (~10 or fewer) // Fortunately, our scenarios tend to have a small number of columns (~10 or fewer)
// each being allocated a large number of pixels (~50 or greater), and // each being allocated a large number of pixels (~50 or greater), and
// people don't even notice the kind of 1-pixel anomolies that are // people don't even notice the kind of 1-pixel anomalies that are
// theoretically inevitable, or don't care if they do. At least they shouldn't // theoretically inevitable, or don't care if they do. At least they shouldn't
// care - no one should be using the results WPF's grid layout to make // care - no one should be using the results WPF's grid layout to make
// quantitative decisions; its job is to produce a reasonable display, not // quantitative decisions; its job is to produce a reasonable display, not
@ -2597,7 +2597,7 @@ namespace Avalonia.Controls
if (scale < 0.0) if (scale < 0.0)
{ {
// if one of the *-weights is Infinity, adjust the weights by mapping // if one of the *-weights is Infinity, adjust the weights by mapping
// Infinty to 1.0 and everything else to 0.0: the infinite items share the // Infinity to 1.0 and everything else to 0.0: the infinite items share the
// available space equally, everyone else gets nothing. // available space equally, everyone else gets nothing.
return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0; return (Double.IsPositiveInfinity(def.UserSize.Value)) ? 1.0 : 0.0;
} }
@ -2655,7 +2655,7 @@ namespace Avalonia.Controls
private enum Flags private enum Flags
{ {
// //
// the foolowing flags let grid tracking dirtiness in more granular manner: // the following flags let grid tracking dirtiness in more granular manner:
// * Valid???Structure flags indicate that elements were added or removed. // * Valid???Structure flags indicate that elements were added or removed.
// * Valid???Layout flags indicate that layout time portion of the information // * Valid???Layout flags indicate that layout time portion of the information
// stored on the objects should be updated. // stored on the objects should be updated.
@ -2684,7 +2684,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// ShowGridLines property. This property is used mostly /// ShowGridLines property. This property is used mostly
/// for simplification of visual debuggig. When it is set /// for simplification of visual debugging. When it is set
/// to <c>true</c> grid lines are drawn to visualize location /// to <c>true</c> grid lines are drawn to visualize location
/// of grid lines. /// of grid lines.
/// </summary> /// </summary>

2
src/Avalonia.Controls/ListBox.cs

@ -89,7 +89,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Note that the selection mode only applies to selections made via user interaction. /// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property. /// Multiple selections can be made programmatically regardless of the value of this property.
/// </remarks> /// </remarks>
public new SelectionMode SelectionMode public new SelectionMode SelectionMode
{ {

2
src/Avalonia.Controls/NativeMenuItemSeparator.cs

@ -3,7 +3,7 @@
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
[Obsolete("This class exists to maintain backwards compatiblity with existing code. Use NativeMenuItemSeparator instead")] [Obsolete("This class exists to maintain backwards compatibility with existing code. Use NativeMenuItemSeparator instead")]
public class NativeMenuItemSeperator : NativeMenuItemSeparator public class NativeMenuItemSeperator : NativeMenuItemSeparator
{ {
} }

2
src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs

@ -25,7 +25,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used. /// Use system chrome where possible. OSX system chrome is used, Windows managed chrome is used.
/// This is because Windows Chrome can not be shown ontop of user content. /// This is because Windows Chrome can not be shown on top of user content.
/// </summary> /// </summary>
PreferSystemChrome = 0x02, PreferSystemChrome = 0x02,

3
src/Avalonia.Controls/Platform/IPlatformLifetimeEventsImpl.cs

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -11,6 +12,6 @@ namespace Avalonia.Platform
/// <remarks> /// <remarks>
/// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit. /// Raised on on OSX via the Quit menu or right-clicking on the application icon and selecting Quit.
/// </remarks> /// </remarks>
event EventHandler<CancelEventArgs> ShutdownRequested; event EventHandler<ShutdownRequestedEventArgs> ShutdownRequested;
} }
} }

37
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -3,11 +3,46 @@ using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Rendering; using Avalonia.Rendering;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
/// <summary>
/// Describes the reason for a <see cref="ITopLevelImpl.Resized"/> message.
/// </summary>
public enum PlatformResizeReason
{
/// <summary>
/// The resize reason is unknown or unspecified.
/// </summary>
Unspecified,
/// <summary>
/// The resize was due to the user resizing the window, for example by dragging the
/// window frame.
/// </summary>
User,
/// <summary>
/// The resize was initiated by the application, for example by setting one of the sizing-
/// related properties on <see cref="Window"/> such as <see cref="Layoutable.Width"/> or
/// <see cref="Layoutable.Height"/>.
/// </summary>
Application,
/// <summary>
/// The resize was initiated by the layout system.
/// </summary>
Layout,
/// <summary>
/// The resize was due to a change in DPI.
/// </summary>
DpiChange,
}
/// <summary> /// <summary>
/// Defines a platform-specific top-level window implementation. /// Defines a platform-specific top-level window implementation.
/// </summary> /// </summary>
@ -57,7 +92,7 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Gets or sets a method called when the toplevel is resized. /// Gets or sets a method called when the toplevel is resized.
/// </summary> /// </summary>
Action<Size> Resized { get; set; } Action<Size, PlatformResizeReason> Resized { get; set; }
/// <summary> /// <summary>
/// Gets or sets a method called when the toplevel's scaling changes. /// Gets or sets a method called when the toplevel's scaling changes.

4
src/Avalonia.Controls/Platform/IWindowBaseImpl.cs

@ -7,7 +7,9 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Shows the window. /// Shows the window.
/// </summary> /// </summary>
void Show(bool activate); /// <param name="activate">Whether to activate the shown window.</param>
/// <param name="isDialog">Whether the window is being shown as a dialog.</param>
void Show(bool activate, bool isDialog);
/// <summary> /// <summary>
/// Hides the window. /// Hides the window.

4
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -110,7 +110,9 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Sets the client size of the top level. /// Sets the client size of the top level.
/// </summary> /// </summary>
void Resize(Size clientSize); /// <param name="clientSize">The new client size.</param>
/// <param name="reason">The reason for the resize.</param>
void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application);
/// <summary> /// <summary>
/// Sets the client size of the top level. /// Sets the client size of the top level.

6
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@ -21,10 +21,12 @@ namespace Avalonia.Controls.Platform
public void RunLoop(CancellationToken cancellationToken) public void RunLoop(CancellationToken cancellationToken)
{ {
while (true) var handles = new[] { _signaled, cancellationToken.WaitHandle };
while (!cancellationToken.IsCancellationRequested)
{ {
Signaled?.Invoke(null); Signaled?.Invoke(null);
_signaled.WaitOne(); WaitHandle.WaitAny(handles);
} }
} }

2
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -183,7 +183,7 @@ namespace Avalonia.Controls.Primitives
info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x => info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x =>
{ {
info.Bounds = x; info.Bounds = x;
InvalidateArrange(); InvalidateMeasure();
}); });
} }
} }

5
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -140,10 +140,5 @@ namespace Avalonia.Controls.Primitives
return new OverlayPopupHost(overlayLayer); return new OverlayPopupHost(overlayLayer);
} }
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size));
}
} }
} }

16
src/Avalonia.Controls/Primitives/Popup.cs

@ -2,6 +2,7 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input; using Avalonia.Input;
@ -18,7 +19,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Displays a popup window. /// Displays a popup window.
/// </summary> /// </summary>
public class Popup : Control, IVisualTreeHost public class Popup : Control, IVisualTreeHost, IPopupHostProvider
{ {
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty = public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), true); AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), true);
@ -134,6 +135,7 @@ namespace Avalonia.Controls.Primitives
private bool _ignoreIsOpenChanged; private bool _ignoreIsOpenChanged;
private PopupOpenState? _openState; private PopupOpenState? _openState;
private IInputElement _overlayInputPassThroughElement; private IInputElement _overlayInputPassThroughElement;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Popup"/> class. /// Initializes static members of the <see cref="Popup"/> class.
@ -351,6 +353,14 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot; IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
IPopupHost? IPopupHostProvider.PopupHost => Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
/// <summary> /// <summary>
/// Opens the popup. /// Opens the popup.
/// </summary> /// </summary>
@ -482,6 +492,8 @@ namespace Avalonia.Controls.Primitives
} }
Opened?.Invoke(this, EventArgs.Empty); Opened?.Invoke(this, EventArgs.Empty);
_popupHostChangedHandler?.Invoke(Host);
} }
/// <summary> /// <summary>
@ -591,6 +603,8 @@ namespace Avalonia.Controls.Primitives
_openState.Dispose(); _openState.Dispose();
_openState = null; _openState = null;
_popupHostChangedHandler?.Invoke(null);
using (BeginIgnoringIsOpen()) using (BeginIgnoringIsOpen())
{ {
IsOpen = false; IsOpen = false;

2
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -27,7 +27,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
/// <summary> /// <summary>
/// An <see cref="IPopupPositioner"/> implementation for platforms on which a popup can be /// An <see cref="IPopupPositioner"/> implementation for platforms on which a popup can be
/// aritrarily positioned. /// arbitrarily positioned.
/// </summary> /// </summary>
public class ManagedPopupPositioner : IPopupPositioner public class ManagedPopupPositioner : IPopupPositioner
{ {

9
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -161,12 +161,9 @@ namespace Avalonia.Controls.Primitives
protected override sealed Size ArrangeSetBounds(Size size) protected override sealed Size ArrangeSetBounds(Size size)
{ {
using (BeginAutoSizing()) _positionerParameters.Size = size;
{ UpdatePosition();
_positionerParameters.Size = size; return ClientSize;
UpdatePosition();
return ClientSize;
}
} }
} }
} }

2
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -170,7 +170,7 @@ namespace Avalonia.Controls.Primitives
} }
/// <summary> /// <summary>
/// Checks if the double value is not inifinity nor NaN. /// Checks if the double value is not infinity nor NaN.
/// </summary> /// </summary>
/// <param name="value">The value.</param> /// <param name="value">The value.</param>
private static bool ValidateDouble(double value) private static bool ValidateDouble(double value)

34
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -57,6 +57,18 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<bool> AllowAutoHideProperty = public static readonly StyledProperty<bool> AllowAutoHideProperty =
AvaloniaProperty.Register<ScrollBar, bool>(nameof(AllowAutoHide), true); AvaloniaProperty.Register<ScrollBar, bool>(nameof(AllowAutoHide), true);
/// <summary>
/// Defines the <see cref="HideDelay"/> property.
/// </summary>
public static readonly StyledProperty<TimeSpan> HideDelayProperty =
AvaloniaProperty.Register<ScrollBar, TimeSpan>(nameof(HideDelay), TimeSpan.FromSeconds(2));
/// <summary>
/// Defines the <see cref="ShowDelay"/> property.
/// </summary>
public static readonly StyledProperty<TimeSpan> ShowDelayProperty =
AvaloniaProperty.Register<ScrollBar, TimeSpan>(nameof(ShowDelay), TimeSpan.FromSeconds(0.5));
private Button _lineUpButton; private Button _lineUpButton;
private Button _lineDownButton; private Button _lineDownButton;
private Button _pageUpButton; private Button _pageUpButton;
@ -126,6 +138,24 @@ namespace Avalonia.Controls.Primitives
get => GetValue(AllowAutoHideProperty); get => GetValue(AllowAutoHideProperty);
set => SetValue(AllowAutoHideProperty, value); set => SetValue(AllowAutoHideProperty, value);
} }
/// <summary>
/// Gets a value that determines how long will be the hide delay after user stops interacting with the scrollbar.
/// </summary>
public TimeSpan HideDelay
{
get => GetValue(HideDelayProperty);
set => SetValue(HideDelayProperty, value);
}
/// <summary>
/// Gets a value that determines how long will be the show delay when user starts interacting with the scrollbar.
/// </summary>
public TimeSpan ShowDelay
{
get => GetValue(ShowDelayProperty);
set => SetValue(ShowDelayProperty, value);
}
public event EventHandler<ScrollEventArgs> Scroll; public event EventHandler<ScrollEventArgs> Scroll;
@ -296,12 +326,12 @@ namespace Avalonia.Controls.Primitives
private void CollapseAfterDelay() private void CollapseAfterDelay()
{ {
InvokeAfterDelay(Collapse, TimeSpan.FromSeconds(2)); InvokeAfterDelay(Collapse, HideDelay);
} }
private void ExpandAfterDelay() private void ExpandAfterDelay()
{ {
InvokeAfterDelay(Expand, TimeSpan.FromMilliseconds(400)); InvokeAfterDelay(Expand, ShowDelay);
} }
private void Collapse() private void Collapse()

2
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -328,7 +328,7 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Note that the selection mode only applies to selections made via user interaction. /// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property. /// Multiple selections can be made programmatically regardless of the value of this property.
/// </remarks> /// </remarks>
protected SelectionMode SelectionMode protected SelectionMode SelectionMode
{ {

40
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -5,7 +5,8 @@ using Avalonia.Logging;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -17,13 +18,13 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> BackgroundProperty = public static readonly StyledProperty<IBrush?> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TemplatedControl>(); Border.BackgroundProperty.AddOwner<TemplatedControl>();
/// <summary> /// <summary>
/// Defines the <see cref="BorderBrush"/> property. /// Defines the <see cref="BorderBrush"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> BorderBrushProperty = public static readonly StyledProperty<IBrush?> BorderBrushProperty =
Border.BorderBrushProperty.AddOwner<TemplatedControl>(); Border.BorderBrushProperty.AddOwner<TemplatedControl>();
/// <summary> /// <summary>
@ -32,6 +33,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<Thickness> BorderThicknessProperty = public static readonly StyledProperty<Thickness> BorderThicknessProperty =
Border.BorderThicknessProperty.AddOwner<TemplatedControl>(); Border.BorderThicknessProperty.AddOwner<TemplatedControl>();
/// <summary>
/// Defines the <see cref="CornerRadius"/> property.
/// </summary>
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<TemplatedControl>();
/// <summary> /// <summary>
/// Defines the <see cref="FontFamily"/> property. /// Defines the <see cref="FontFamily"/> property.
/// </summary> /// </summary>
@ -59,7 +66,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="Foreground"/> property. /// Defines the <see cref="Foreground"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> ForegroundProperty = public static readonly StyledProperty<IBrush?> ForegroundProperty =
TextBlock.ForegroundProperty.AddOwner<TemplatedControl>(); TextBlock.ForegroundProperty.AddOwner<TemplatedControl>();
/// <summary> /// <summary>
@ -71,8 +78,8 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="Template"/> property. /// Defines the <see cref="Template"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IControlTemplate> TemplateProperty = public static readonly StyledProperty<IControlTemplate?> TemplateProperty =
AvaloniaProperty.Register<TemplatedControl, IControlTemplate>(nameof(Template)); AvaloniaProperty.Register<TemplatedControl, IControlTemplate?>(nameof(Template));
/// <summary> /// <summary>
/// Defines the IsTemplateFocusTarget attached property. /// Defines the IsTemplateFocusTarget attached property.
@ -88,7 +95,7 @@ namespace Avalonia.Controls.Primitives
"TemplateApplied", "TemplateApplied",
RoutingStrategies.Direct); RoutingStrategies.Direct);
private IControlTemplate _appliedTemplate; private IControlTemplate? _appliedTemplate;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TemplatedControl"/> class. /// Initializes static members of the <see cref="TemplatedControl"/> class.
@ -111,7 +118,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets the brush used to draw the control's background. /// Gets or sets the brush used to draw the control's background.
/// </summary> /// </summary>
public IBrush Background public IBrush? Background
{ {
get { return GetValue(BackgroundProperty); } get { return GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); } set { SetValue(BackgroundProperty, value); }
@ -120,7 +127,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets the brush used to draw the control's border. /// Gets or sets the brush used to draw the control's border.
/// </summary> /// </summary>
public IBrush BorderBrush public IBrush? BorderBrush
{ {
get { return GetValue(BorderBrushProperty); } get { return GetValue(BorderBrushProperty); }
set { SetValue(BorderBrushProperty, value); } set { SetValue(BorderBrushProperty, value); }
@ -135,6 +142,15 @@ namespace Avalonia.Controls.Primitives
set { SetValue(BorderThicknessProperty, value); } set { SetValue(BorderThicknessProperty, value); }
} }
/// <summary>
/// Gets or sets the radius of the border rounded corners.
/// </summary>
public CornerRadius CornerRadius
{
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary> /// <summary>
/// Gets or sets the font family used to draw the control's text. /// Gets or sets the font family used to draw the control's text.
/// </summary> /// </summary>
@ -174,7 +190,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets the brush used to draw the control's text and other foreground elements. /// Gets or sets the brush used to draw the control's text and other foreground elements.
/// </summary> /// </summary>
public IBrush Foreground public IBrush? Foreground
{ {
get { return GetValue(ForegroundProperty); } get { return GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); } set { SetValue(ForegroundProperty, value); }
@ -192,7 +208,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets the template that defines the control's appearance. /// Gets or sets the template that defines the control's appearance.
/// </summary> /// </summary>
public IControlTemplate Template public IControlTemplate? Template
{ {
get { return GetValue(TemplateProperty); } get { return GetValue(TemplateProperty); }
set { SetValue(TemplateProperty, value); } set { SetValue(TemplateProperty, value); }
@ -265,7 +281,9 @@ namespace Avalonia.Controls.Primitives
var e = new TemplateAppliedEventArgs(nameScope); var e = new TemplateAppliedEventArgs(nameScope);
OnApplyTemplate(e); OnApplyTemplate(e);
#pragma warning disable CS0618 // Type or member is obsolete
OnTemplateApplied(e); OnTemplateApplied(e);
#pragma warning restore CS0618 // Type or member is obsolete
RaiseEvent(e); RaiseEvent(e);
} }

2
src/Avalonia.Controls/Repeater/IElementFactory.cs

@ -46,7 +46,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// A data template that supports creating and recyling elements for an <see cref="ItemsRepeater"/>. /// A data template that supports creating and recycling elements for an <see cref="ItemsRepeater"/>.
/// </summary> /// </summary>
public interface IElementFactory : IDataTemplate public interface IElementFactory : IDataTemplate
{ {

4
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -441,9 +441,9 @@ namespace Avalonia.Controls
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
internal IControl GetElementImpl(int index, bool forceCreate, bool supressAutoRecycle) internal IControl GetElementImpl(int index, bool forceCreate, bool suppressAutoRecycle)
{ {
var element = _viewManager.GetElement(index, forceCreate, supressAutoRecycle); var element = _viewManager.GetElement(index, forceCreate, suppressAutoRecycle);
return element; return element;
} }

2
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -174,7 +174,7 @@ namespace Avalonia.Controls
} }
else else
{ {
// We could not find a candiate. // We could not find a candidate.
_lastFocusedElement = null; _lastFocusedElement = null;
} }
} }

2
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -186,7 +186,7 @@ namespace Avalonia.Controls
_expectedViewportShift.X + _layoutExtent.X - extent.X, _expectedViewportShift.X + _layoutExtent.X - extent.X,
_expectedViewportShift.Y + _layoutExtent.Y - extent.Y); _expectedViewportShift.Y + _layoutExtent.Y - extent.Y);
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. // We tolerate viewport imprecisions up to 1 pixel to avoid invalidating layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})", Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})",

2
src/Avalonia.Controls/ScrollViewer.cs

@ -653,7 +653,7 @@ namespace Avalonia.Controls
private void CalculatedPropertiesChanged() private void CalculatedPropertiesChanged()
{ {
// Pass old values of 0 here because we don't have the old values at this point, // Pass old values of 0 here because we don't have the old values at this point,
// and it shouldn't matter as only the template uses these properies. // and it shouldn't matter as only the template uses these properties.
RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum); RaisePropertyChanged(HorizontalScrollBarMaximumProperty, 0, HorizontalScrollBarMaximum);
RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue); RaisePropertyChanged(HorizontalScrollBarValueProperty, 0, HorizontalScrollBarValue);
RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize); RaisePropertyChanged(HorizontalScrollBarViewportSizeProperty, 0, HorizontalScrollBarViewportSize);

2
src/Avalonia.Controls/Selection/SelectionModel.cs

@ -345,7 +345,7 @@ namespace Avalonia.Controls.Selection
private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems) private protected override void OnSelectionChanged(IReadOnlyList<T> deselectedItems)
{ {
// Note: We're *not* putting this in a using scope. A collection update is still in progress // Note: We're *not* putting this in a using scope. A collection update is still in progress
// so the operation won't get commited by normal means: we have to commit it manually. // so the operation won't get committed by normal means: we have to commit it manually.
var update = BatchUpdate(); var update = BatchUpdate();
update.Operation.DeselectedItems = deselectedItems; update.Operation.DeselectedItems = deselectedItems;

65
src/Avalonia.Controls/TextBox.cs

@ -31,7 +31,7 @@ namespace Avalonia.Controls
public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current public static KeyGesture PasteGesture { get; } = AvaloniaLocator.Current
.GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault(); .GetService<PlatformHotkeyConfiguration>()?.Paste.FirstOrDefault();
public static readonly StyledProperty<bool> AcceptsReturnProperty = public static readonly StyledProperty<bool> AcceptsReturnProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn)); AvaloniaProperty.Register<TextBox, bool>(nameof(AcceptsReturn));
@ -117,7 +117,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> RevealPasswordProperty = public static readonly StyledProperty<bool> RevealPasswordProperty =
AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword)); AvaloniaProperty.Register<TextBox, bool>(nameof(RevealPassword));
public static readonly DirectProperty<TextBox, bool> CanCutProperty = public static readonly DirectProperty<TextBox, bool> CanCutProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>( AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCut), nameof(CanCut),
@ -135,7 +135,7 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsUndoEnabledProperty = public static readonly StyledProperty<bool> IsUndoEnabledProperty =
AvaloniaProperty.Register<TextBox, bool>( AvaloniaProperty.Register<TextBox, bool>(
nameof(IsUndoEnabled), nameof(IsUndoEnabled),
defaultValue: true); defaultValue: true);
public static readonly DirectProperty<TextBox, int> UndoLimitProperty = public static readonly DirectProperty<TextBox, int> UndoLimitProperty =
@ -157,6 +157,10 @@ namespace Avalonia.Controls
} }
public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text);
public override bool Equals(object obj) => obj is UndoRedoState other && Equals(other);
public override int GetHashCode() => Text.GetHashCode();
} }
private string _text; private string _text;
@ -174,6 +178,10 @@ namespace Avalonia.Controls
private string _newLine = Environment.NewLine; private string _newLine = Environment.NewLine;
private static readonly string[] invalidCharacters = new String[1] { "\u007f" }; private static readonly string[] invalidCharacters = new String[1] { "\u007f" };
private int _selectedTextChangesMadeSinceLastUndoSnapshot;
private bool _hasDoneSnapshotOnce;
private const int _maxCharsBeforeUndoSnapshot = 7;
static TextBox() static TextBox()
{ {
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
@ -202,7 +210,8 @@ namespace Avalonia.Controls
horizontalScrollBarVisibility, horizontalScrollBarVisibility,
BindingPriority.Style); BindingPriority.Style);
_undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this); _undoRedoHelper = new UndoRedoHelper<UndoRedoState>(this);
_selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = false;
UpdatePseudoclasses(); UpdatePseudoclasses();
} }
@ -331,6 +340,7 @@ namespace Avalonia.Controls
if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
{ {
_undoRedoHelper.Clear(); _undoRedoHelper.Clear();
SnapshotUndoRedo(); // so we always have an initial state
} }
} }
} }
@ -341,16 +351,16 @@ namespace Avalonia.Controls
get { return GetSelection(); } get { return GetSelection(); }
set set
{ {
SnapshotUndoRedo();
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
_selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false);
DeleteSelection(); DeleteSelection();
} }
else else
{ {
HandleTextInput(value); HandleTextInput(value);
} }
SnapshotUndoRedo();
} }
} }
@ -422,7 +432,7 @@ namespace Avalonia.Controls
get { return _newLine; } get { return _newLine; }
set { SetAndRaise(NewLineProperty, ref _newLine, value); } set { SetAndRaise(NewLineProperty, ref _newLine, value); }
} }
/// <summary> /// <summary>
/// Clears the current selection, maintaining the <see cref="CaretIndex"/> /// Clears the current selection, maintaining the <see cref="CaretIndex"/>
/// </summary> /// </summary>
@ -480,11 +490,13 @@ namespace Avalonia.Controls
var oldValue = _undoRedoHelper.Limit; var oldValue = _undoRedoHelper.Limit;
_undoRedoHelper.Limit = value; _undoRedoHelper.Limit = value;
RaisePropertyChanged(UndoLimitProperty, oldValue, value); RaisePropertyChanged(UndoLimitProperty, oldValue, value);
} }
// from docs at // from docs at
// https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled: // https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.textboxbase.isundoenabled:
// "Setting UndoLimit clears the undo queue." // "Setting UndoLimit clears the undo queue."
_undoRedoHelper.Clear(); _undoRedoHelper.Clear();
_selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = false;
} }
} }
@ -515,6 +527,8 @@ namespace Avalonia.Controls
// Therefore, if you disable undo and then re-enable it, undo commands still do not work // Therefore, if you disable undo and then re-enable it, undo commands still do not work
// because the undo stack was emptied when you disabled undo." // because the undo stack was emptied when you disabled undo."
_undoRedoHelper.Clear(); _undoRedoHelper.Clear();
_selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = false;
} }
} }
@ -577,23 +591,25 @@ namespace Avalonia.Controls
{ {
return; return;
} }
input = RemoveInvalidCharacters(input); input = RemoveInvalidCharacters(input);
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
{ {
return; return;
} }
_selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false);
string text = Text ?? string.Empty; string text = Text ?? string.Empty;
int caretIndex = CaretIndex; int caretIndex = CaretIndex;
int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd); int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength) if (MaxLength > 0 && newLength > MaxLength)
{ {
input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength))); input = input.Remove(Math.Max(0, input.Length - (newLength - MaxLength)));
} }
if (!string.IsNullOrEmpty(input)) if (!string.IsNullOrEmpty(input))
{ {
DeleteSelection(); DeleteSelection();
@ -627,7 +643,6 @@ namespace Avalonia.Controls
SnapshotUndoRedo(); SnapshotUndoRedo();
Copy(); Copy();
DeleteSelection(); DeleteSelection();
SnapshotUndoRedo();
} }
public async void Copy() public async void Copy()
@ -647,7 +662,6 @@ namespace Avalonia.Controls
SnapshotUndoRedo(); SnapshotUndoRedo();
HandleTextInput(text); HandleTextInput(text);
SnapshotUndoRedo();
} }
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
@ -696,6 +710,7 @@ namespace Avalonia.Controls
{ {
try try
{ {
SnapshotUndoRedo();
_isUndoingRedoing = true; _isUndoingRedoing = true;
_undoRedoHelper.Undo(); _undoRedoHelper.Undo();
} }
@ -830,7 +845,6 @@ namespace Avalonia.Controls
CaretIndex -= removedCharacters; CaretIndex -= removedCharacters;
ClearSelection(); ClearSelection();
} }
SnapshotUndoRedo();
handled = true; handled = true;
break; break;
@ -858,7 +872,6 @@ namespace Avalonia.Controls
SetTextInternal(text.Substring(0, caretIndex) + SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters)); text.Substring(caretIndex + removedCharacters));
} }
SnapshotUndoRedo();
handled = true; handled = true;
break; break;
@ -868,7 +881,6 @@ namespace Avalonia.Controls
{ {
SnapshotUndoRedo(); SnapshotUndoRedo();
HandleTextInput(NewLine); HandleTextInput(NewLine);
SnapshotUndoRedo();
handled = true; handled = true;
} }
@ -879,7 +891,6 @@ namespace Avalonia.Controls
{ {
SnapshotUndoRedo(); SnapshotUndoRedo();
HandleTextInput("\t"); HandleTextInput("\t");
SnapshotUndoRedo();
handled = true; handled = true;
} }
else else
@ -889,6 +900,10 @@ namespace Avalonia.Controls
break; break;
case Key.Space:
SnapshotUndoRedo(); // always snapshot in between words
break;
default: default:
handled = false; handled = false;
break; break;
@ -1319,11 +1334,19 @@ namespace Avalonia.Controls
} }
} }
private void SnapshotUndoRedo() private void SnapshotUndoRedo(bool ignoreChangeCount = true)
{ {
if (IsUndoEnabled) if (IsUndoEnabled)
{ {
_undoRedoHelper.Snapshot(); if (ignoreChangeCount ||
!_hasDoneSnapshotOnce ||
(!ignoreChangeCount &&
_selectedTextChangesMadeSinceLastUndoSnapshot >= _maxCharsBeforeUndoSnapshot))
{
_undoRedoHelper.Snapshot();
_selectedTextChangesMadeSinceLastUndoSnapshot = 0;
_hasDoneSnapshotOnce = true;
}
} }
} }
} }

8
src/Avalonia.Controls/TickBar.cs

@ -193,7 +193,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or
/// top and bottom spacing (for vertical orienation). /// top and bottom spacing (for vertical orientation).
/// The space on both sides of TickBar is half of specified ReservedSpace. /// The space on both sides of TickBar is half of specified ReservedSpace.
/// This property has type of <see cref="Rect" />. /// This property has type of <see cref="Rect" />.
/// </summary> /// </summary>
@ -210,7 +210,7 @@ namespace Avalonia.Controls
/// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and
/// SelectionStart and SelectionEnd are valid. /// SelectionStart and SelectionEnd are valid.
/// ///
/// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height /// The primary ticks (for Minimum and Maximum value) height will be 100% of TickBar's render size (use Width or Height
/// depends on Placement property). /// depends on Placement property).
/// ///
/// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size. /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size.
@ -221,7 +221,7 @@ namespace Avalonia.Controls
{ {
var size = new Size(Bounds.Width, Bounds.Height); var size = new Size(Bounds.Width, Bounds.Height);
var range = Maximum - Minimum; var range = Maximum - Minimum;
var tickLen = 0.0d; // Height for Primary Tick (for Mininum and Maximum value) var tickLen = 0.0d; // Height for Primary Tick (for Minimum and Maximum value)
var tickLen2 = 0.0d; // Height for Secondary Tick var tickLen2 = 0.0d; // Height for Secondary Tick
var logicalToPhysical = 1.0; var logicalToPhysical = 1.0;
var startPoint = new Point(); var startPoint = new Point();
@ -285,7 +285,7 @@ namespace Avalonia.Controls
tickLen2 = tickLen * 0.75; tickLen2 = tickLen * 0.75;
// Invert direciton of the ticks // Invert direction of the ticks
if (IsDirectionReversed) if (IsDirectionReversed)
{ {
logicalToPhysical *= -1; logicalToPhysical *= -1;

40
src/Avalonia.Controls/ToolTip.cs

@ -1,9 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using System.Reactive.Linq; using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -17,7 +16,7 @@ namespace Avalonia.Controls
/// assigning the content that you want displayed. /// assigning the content that you want displayed.
/// </remarks> /// </remarks>
[PseudoClasses(":open")] [PseudoClasses(":open")]
public class ToolTip : ContentControl public class ToolTip : ContentControl, IPopupHostProvider
{ {
/// <summary> /// <summary>
/// Defines the ToolTip.Tip attached property. /// Defines the ToolTip.Tip attached property.
@ -61,7 +60,8 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty<ToolTip?> ToolTipProperty = internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip"); AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost? _popup; private IPopupHost? _popupHost;
private Action<IPopupHost?>? _popupHostChangedHandler;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class. /// Initializes static members of the <see cref="ToolTip"/> class.
@ -251,35 +251,45 @@ namespace Avalonia.Controls
tooltip.RecalculatePosition(control); tooltip.RecalculatePosition(control);
} }
IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
internal void RecalculatePosition(Control control) internal void RecalculatePosition(Control control)
{ {
_popup?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
} }
private void Open(Control control) private void Open(Control control)
{ {
Close(); Close();
_popup = OverlayPopupHost.CreatePopupHost(control, null); _popupHost = OverlayPopupHost.CreatePopupHost(control, null);
_popup.SetChild(this); _popupHost.SetChild(this);
((ISetLogicalParent)_popup).SetParent(control); ((ISetLogicalParent)_popupHost).SetParent(control);
_popup.ConfigurePosition(control, GetPlacement(control), _popupHost.ConfigurePosition(control, GetPlacement(control),
new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
WindowManagerAddShadowHintChanged(_popup, false); WindowManagerAddShadowHintChanged(_popupHost, false);
_popup.Show(); _popupHost.Show();
_popupHostChangedHandler?.Invoke(_popupHost);
} }
private void Close() private void Close()
{ {
if (_popup != null) if (_popupHost != null)
{ {
_popup.SetChild(null); _popupHost.SetChild(null);
_popup.Dispose(); _popupHost.Dispose();
_popup = null; _popupHost = null;
_popupHostChangedHandler?.Invoke(null);
} }
} }

9
src/Avalonia.Controls/TopLevel.cs

@ -90,6 +90,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static TopLevel() static TopLevel()
{ {
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TopLevel>(KeyboardNavigationMode.Cycle);
AffectsMeasure<TopLevel>(ClientSizeProperty); AffectsMeasure<TopLevel>(ClientSizeProperty);
TransparencyLevelHintProperty.Changed.AddClassHandler<TopLevel>( TransparencyLevelHintProperty.Changed.AddClassHandler<TopLevel>(
@ -224,7 +225,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Gets the acheived <see cref="WindowTransparencyLevel"/> that the platform was able to provide. /// Gets the achieved <see cref="WindowTransparencyLevel"/> that the platform was able to provide.
/// </summary> /// </summary>
public WindowTransparencyLevel ActualTransparencyLevel public WindowTransparencyLevel ActualTransparencyLevel
{ {
@ -376,11 +377,15 @@ namespace Avalonia.Controls
LayoutManager?.Dispose(); LayoutManager?.Dispose();
} }
[Obsolete("Use HandleResized(Size, PlatformResizeReason)")]
protected virtual void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified);
/// <summary> /// <summary>
/// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>. /// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary> /// </summary>
/// <param name="clientSize">The new client size.</param> /// <param name="clientSize">The new client size.</param>
protected virtual void HandleResized(Size clientSize) /// <param name="reason">The reason for the resize.</param>
protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason)
{ {
ClientSize = clientSize; ClientSize = clientSize;
FrameSize = PlatformImpl.FrameSize; FrameSize = PlatformImpl.FrameSize;

11
src/Avalonia.Controls/Utils/UndoRedoHelper.cs

@ -7,7 +7,7 @@ using Avalonia.Utilities;
namespace Avalonia.Controls.Utils namespace Avalonia.Controls.Utils
{ {
class UndoRedoHelper<TState> : WeakTimer.IWeakTimerSubscriber where TState : struct, IEquatable<TState> class UndoRedoHelper<TState>
{ {
private readonly IUndoRedoHost _host; private readonly IUndoRedoHost _host;
@ -31,7 +31,6 @@ namespace Avalonia.Controls.Utils
public UndoRedoHelper(IUndoRedoHost host) public UndoRedoHelper(IUndoRedoHost host)
{ {
_host = host; _host = host;
WeakTimer.StartWeakTimer(this, TimeSpan.FromSeconds(1));
} }
public void Undo() public void Undo()
@ -61,7 +60,7 @@ namespace Avalonia.Controls.Utils
if (_states.Last != null) if (_states.Last != null)
{ {
_states.Last.Value = state; _states.Last.Value = state;
} }
} }
public void UpdateLastState() public void UpdateLastState()
@ -103,11 +102,5 @@ namespace Avalonia.Controls.Utils
_states.Clear(); _states.Clear();
_currentNode = null; _currentNode = null;
} }
bool WeakTimer.IWeakTimerSubscriber.Tick()
{
Snapshot();
return true;
}
} }
} }

145
src/Avalonia.Controls/Window.cs

@ -244,7 +244,7 @@ namespace Avalonia.Controls
impl.WindowStateChanged = HandleWindowStateChanged; impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x)); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
} }
@ -258,6 +258,18 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets a value indicating how the window will size itself to fit its content. /// Gets or sets a value indicating how the window will size itself to fit its content.
/// </summary> /// </summary>
/// <remarks>
/// If <see cref="SizeToContent"/> has a value other than <see cref="SizeToContent.Manual"/>,
/// <see cref="SizeToContent"/> is automatically set to <see cref="SizeToContent.Manual"/>
/// if a user resizes the window by using the resize grip or dragging the border.
///
/// NOTE: Because of a limitation of X11, <see cref="SizeToContent"/> will be reset on X11 to
/// <see cref="SizeToContent.Manual"/> on any resize - including the resize that happens when
/// the window is first shown. This is because X11 resize notifications are asynchronous and
/// there is no way to know whether a resize came from the user or the layout system. To avoid
/// this, consider setting <see cref="CanResize"/> to false, which will disable user resizing
/// of the window.
/// </remarks>
public SizeToContent SizeToContent public SizeToContent SizeToContent
{ {
get { return GetValue(SizeToContentProperty); } get { return GetValue(SizeToContentProperty); }
@ -583,28 +595,23 @@ namespace Avalonia.Controls
return; return;
} }
using (BeginAutoSizing()) Renderer?.Stop();
{
Renderer?.Stop();
if (Owner is Window owner) if (Owner is Window owner)
{ {
owner.RemoveChild(this); owner.RemoveChild(this);
} }
if (_children.Count > 0) if (_children.Count > 0)
{
foreach (var child in _children.ToArray())
{ {
foreach (var child in _children.ToArray()) child.child.Hide();
{
child.child.Hide();
}
} }
Owner = null;
PlatformImpl?.Hide();
} }
Owner = null;
PlatformImpl?.Hide();
IsVisible = false; IsVisible = false;
} }
@ -675,29 +682,23 @@ namespace Avalonia.Controls
if (initialSize != ClientSize) if (initialSize != ClientSize)
{ {
using (BeginAutoSizing()) PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
{
PlatformImpl?.Resize(initialSize);
}
} }
LayoutManager.ExecuteInitialLayoutPass(); LayoutManager.ExecuteInitialLayoutPass();
using (BeginAutoSizing()) if (parent != null)
{ {
if (parent != null) PlatformImpl?.SetParent(parent.PlatformImpl);
{
PlatformImpl?.SetParent(parent.PlatformImpl);
}
Owner = parent;
parent?.AddChild(this, false);
SetWindowStartupLocation(Owner?.PlatformImpl);
PlatformImpl?.Show(ShowActivated);
Renderer?.Start();
} }
Owner = parent;
parent?.AddChild(this, false);
SetWindowStartupLocation(Owner?.PlatformImpl);
PlatformImpl?.Show(ShowActivated, false);
Renderer?.Start();
OnOpened(EventArgs.Empty); OnOpened(EventArgs.Empty);
} }
@ -760,41 +761,34 @@ namespace Avalonia.Controls
if (initialSize != ClientSize) if (initialSize != ClientSize)
{ {
using (BeginAutoSizing()) PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
{
PlatformImpl?.Resize(initialSize);
}
} }
LayoutManager.ExecuteInitialLayoutPass(); LayoutManager.ExecuteInitialLayoutPass();
var result = new TaskCompletionSource<TResult>(); var result = new TaskCompletionSource<TResult>();
using (BeginAutoSizing()) PlatformImpl?.SetParent(owner.PlatformImpl);
{ Owner = owner;
PlatformImpl?.SetParent(owner.PlatformImpl); owner.AddChild(this, true);
Owner = owner;
owner.AddChild(this, true);
SetWindowStartupLocation(owner.PlatformImpl);
PlatformImpl?.Show(ShowActivated);
Renderer?.Start(); SetWindowStartupLocation(owner.PlatformImpl);
Observable.FromEventPattern<EventHandler, EventArgs>( PlatformImpl?.Show(ShowActivated, true);
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)));
});
OnOpened(EventArgs.Empty); Renderer?.Start();
}
Observable.FromEventPattern<EventHandler, EventArgs>(
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)));
});
OnOpened(EventArgs.Empty);
return result.Task; return result.Task;
} }
@ -937,11 +931,8 @@ namespace Avalonia.Controls
protected sealed override Size ArrangeSetBounds(Size size) protected sealed override Size ArrangeSetBounds(Size size)
{ {
using (BeginAutoSizing()) PlatformImpl?.Resize(size, PlatformResizeReason.Layout);
{ return ClientSize;
PlatformImpl?.Resize(size);
return ClientSize;
}
} }
protected sealed override void HandleClosed() protected sealed override void HandleClosed()
@ -958,18 +949,36 @@ namespace Avalonia.Controls
Owner = null; Owner = null;
} }
[Obsolete("Use HandleResized(Size, PlatformResizeReason)")]
protected sealed override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified);
/// <inheritdoc/> /// <inheritdoc/>
protected sealed override void HandleResized(Size clientSize) protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason)
{ {
if (!AutoSizing) if (ClientSize == clientSize)
return;
var sizeToContent = SizeToContent;
// If auto-sizing is enabled, and the resize came from a user resize (or the reason was
// unspecified) then turn off auto-resizing for any window dimension that is not equal
// to the requested size.
if (sizeToContent != SizeToContent.Manual &&
CanResize &&
reason == PlatformResizeReason.Unspecified ||
reason == PlatformResizeReason.User)
{ {
SizeToContent = SizeToContent.Manual; if (clientSize.Width != ClientSize.Width)
sizeToContent &= ~SizeToContent.Width;
if (clientSize.Height != ClientSize.Height)
sizeToContent &= ~SizeToContent.Height;
SizeToContent = sizeToContent;
} }
Width = clientSize.Width; Width = clientSize.Width;
Height = clientSize.Height; Height = clientSize.Height;
base.HandleResized(clientSize); base.HandleResized(clientSize, reason);
} }
/// <summary> /// <summary>

34
src/Avalonia.Controls/WindowBase.cs

@ -39,7 +39,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> TopmostProperty = public static readonly StyledProperty<bool> TopmostProperty =
AvaloniaProperty.Register<WindowBase, bool>(nameof(Topmost)); AvaloniaProperty.Register<WindowBase, bool>(nameof(Topmost));
private int _autoSizing;
private bool _hasExecutedInitialLayoutPass; private bool _hasExecutedInitialLayoutPass;
private bool _isActive; private bool _isActive;
private bool _ignoreVisibilityChange; private bool _ignoreVisibilityChange;
@ -95,10 +94,8 @@ namespace Avalonia.Controls
public Screens Screens { get; private set; } public Screens Screens { get; private set; }
/// <summary> [Obsolete("No longer used. Always returns false.")]
/// Whether an auto-size operation is in progress. protected bool AutoSizing => false;
/// </summary>
protected bool AutoSizing => _autoSizing > 0;
/// <summary> /// <summary>
/// Gets or sets the owner of the window. /// Gets or sets the owner of the window.
@ -162,7 +159,7 @@ namespace Avalonia.Controls
LayoutManager.ExecuteInitialLayoutPass(); LayoutManager.ExecuteInitialLayoutPass();
_hasExecutedInitialLayoutPass = true; _hasExecutedInitialLayoutPass = true;
} }
PlatformImpl?.Show(true); PlatformImpl?.Show(true, false);
Renderer?.Start(); Renderer?.Start();
OnOpened(EventArgs.Empty); OnOpened(EventArgs.Empty);
} }
@ -172,20 +169,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Begins an auto-resize operation. [Obsolete("No longer used. Has no effect.")]
/// </summary> protected IDisposable BeginAutoSizing() => Disposable.Empty;
/// <returns>A disposable used to finish the operation.</returns>
/// <remarks>
/// When an auto-resize operation is in progress any resize events received will not be
/// cause the new size to be written to the <see cref="Layoutable.Width"/> and
/// <see cref="Layoutable.Height"/> properties.
/// </remarks>
protected IDisposable BeginAutoSizing()
{
++_autoSizing;
return Disposable.Create(() => --_autoSizing);
}
/// <summary> /// <summary>
/// Ensures that the window is initialized. /// Ensures that the window is initialized.
@ -215,11 +201,15 @@ namespace Avalonia.Controls
} }
} }
[Obsolete("Use HandleResized(Size, PlatformResizeReason)")]
protected override void HandleResized(Size clientSize) => HandleResized(clientSize, PlatformResizeReason.Unspecified);
/// <summary> /// <summary>
/// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>. /// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary> /// </summary>
/// <param name="clientSize">The new client size.</param> /// <param name="clientSize">The new client size.</param>
protected override void HandleResized(Size clientSize) /// <param name="reason">The reason for the resize.</param>
protected override void HandleResized(Size clientSize, PlatformResizeReason reason)
{ {
ClientSize = clientSize; ClientSize = clientSize;
FrameSize = PlatformImpl.FrameSize; FrameSize = PlatformImpl.FrameSize;
@ -264,7 +254,7 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// Called durung the arrange pass to set the size of the window. /// Called during the arrange pass to set the size of the window.
/// </summary> /// </summary>
/// <param name="size">The requested size of the window.</param> /// <param name="size">The requested size of the window.</param>
/// <returns>The actual size of the window.</returns> /// <returns>The actual size of the window.</returns>

8
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -20,7 +20,7 @@ namespace Avalonia.DesignerSupport.Remote
ClientSize = new Size(1, 1); ClientSize = new Size(1, 1);
} }
public void Show(bool activate) public void Show(bool activate, bool isDialog)
{ {
} }
@ -58,7 +58,7 @@ namespace Avalonia.DesignerSupport.Remote
base.OnMessage(transport, obj); base.OnMessage(transport, obj);
} }
public void Resize(Size clientSize) public void Resize(Size clientSize, PlatformResizeReason reason)
{ {
_transport.Send(new RequestViewportResizeMessage _transport.Send(new RequestViewportResizeMessage
{ {
@ -99,10 +99,6 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void ShowDialog(IWindowImpl parent)
{
}
public void SetSystemDecorations(SystemDecorations enabled) public void SetSystemDecorations(SystemDecorations enabled)
{ {
} }

8
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -27,7 +27,7 @@ namespace Avalonia.DesignerSupport.Remote
public IEnumerable<object> Surfaces { get; } public IEnumerable<object> Surfaces { get; }
public Action<RawInputEventArgs> Input { get; set; } public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size, PlatformResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; } public Action<double> ScalingChanged { get; set; }
public Func<bool> Closing { get; set; } public Func<bool> Closing { get; set; }
public Action Closed { get; set; } public Action Closed { get; set; }
@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent,
(_, size, __) => (_, size, __) =>
{ {
Resize(size); Resize(size, PlatformResizeReason.Unspecified);
})); }));
} }
@ -78,7 +78,7 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void Show(bool activate) public void Show(bool activate, bool isDialog)
{ {
} }
@ -98,7 +98,7 @@ namespace Avalonia.DesignerSupport.Remote
{ {
} }
public void Resize(Size clientSize) public void Resize(Size clientSize, PlatformResizeReason reason)
{ {
} }

8
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,6 +1,5 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Diagnostics.Models; using Avalonia.Diagnostics.Models;
using Avalonia.Input; using Avalonia.Input;
@ -22,6 +21,7 @@ namespace Avalonia.Diagnostics.ViewModels
private bool _shouldVisualizeMarginPadding = true; private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects; private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay; private bool _showFpsOverlay;
private bool _freezePopups;
private bool _showPropertyType; private bool _showPropertyType;
#nullable disable #nullable disable
@ -42,6 +42,12 @@ namespace Avalonia.Diagnostics.ViewModels
Console = new ConsoleViewModel(UpdateConsoleContext); Console = new ConsoleViewModel(UpdateConsoleContext);
} }
public bool FreezePopups
{
get => _freezePopups;
set => RaiseAndSetIfChanged(ref _freezePopups, value);
}
public bool ShouldVisualizeMarginPadding public bool ShouldVisualizeMarginPadding
{ {
get => _shouldVisualizeMarginPadding; get => _shouldVisualizeMarginPadding;

33
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -1,26 +1,28 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels namespace Avalonia.Diagnostics.ViewModels
{ {
internal abstract class TreeNode : ViewModelBase, IDisposable internal abstract class TreeNode : ViewModelBase, IDisposable
{ {
private IDisposable? _classesSubscription; private readonly IDisposable? _classesSubscription;
private string _classes; private string _classes;
private bool _isExpanded; private bool _isExpanded;
public TreeNode(IVisual visual, TreeNode? parent) protected TreeNode(IVisual visual, TreeNode? parent, string? customName = null)
{ {
_classes = string.Empty;
Parent = parent; Parent = parent;
Type = visual.GetType().Name; Type = customName ?? visual.GetType().Name;
Visual = visual; Visual = visual;
_classes = string.Empty; FontWeight = IsRoot ? FontWeight.Bold : FontWeight.Normal;
if (visual is IControl control) if (visual is IControl control)
{ {
@ -52,6 +54,12 @@ namespace Avalonia.Diagnostics.ViewModels
} }
} }
private bool IsRoot => Visual is TopLevel ||
Visual is ContextMenu ||
Visual is IPopupHost;
public FontWeight FontWeight { get; }
public abstract TreeNodeCollection Children public abstract TreeNodeCollection Children
{ {
get; get;
@ -95,20 +103,5 @@ namespace Avalonia.Diagnostics.ViewModels
_classesSubscription?.Dispose(); _classesSubscription?.Dispose();
Children.Dispose(); Children.Dispose();
} }
private static int IndexOf(IReadOnlyList<TreeNode> collection, TreeNode item)
{
var count = collection.Count;
for (var i = 0; i < count; ++i)
{
if (collection[i] == item)
{
return i;
}
}
throw new AvaloniaInternalException("TreeNode was not present in parent Children collection.");
}
} }
} }

123
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -1,5 +1,10 @@
using System; using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -7,31 +12,30 @@ namespace Avalonia.Diagnostics.ViewModels
{ {
internal class VisualTreeNode : TreeNode internal class VisualTreeNode : TreeNode
{ {
public VisualTreeNode(IVisual visual, TreeNode? parent) public VisualTreeNode(IVisual visual, TreeNode? parent, string? customName = null)
: base(visual, parent) : base(visual, parent, customName)
{ {
Children = new VisualTreeNodeCollection(this, visual); Children = new VisualTreeNodeCollection(this, visual);
if ((Visual is IStyleable styleable)) if (Visual is IStyleable styleable)
{
IsInTemplate = styleable.TemplatedParent != null; IsInTemplate = styleable.TemplatedParent != null;
}
} }
public bool IsInTemplate { get; private set; } public bool IsInTemplate { get; }
public override TreeNodeCollection Children { get; } public override TreeNodeCollection Children { get; }
public static VisualTreeNode[] Create(object control) public static VisualTreeNode[] Create(object control)
{ {
var visual = control as IVisual; return control is IVisual visual ?
return visual != null ? new[] { new VisualTreeNode(visual, null) } : Array.Empty<VisualTreeNode>(); new[] { new VisualTreeNode(visual, null) } :
Array.Empty<VisualTreeNode>();
} }
internal class VisualTreeNodeCollection : TreeNodeCollection internal class VisualTreeNodeCollection : TreeNodeCollection
{ {
private readonly IVisual _control; private readonly IVisual _control;
private IDisposable? _subscription; private readonly CompositeDisposable _subscriptions = new CompositeDisposable(2);
public VisualTreeNodeCollection(TreeNode owner, IVisual control) public VisualTreeNodeCollection(TreeNode owner, IVisual control)
: base(owner) : base(owner)
@ -41,15 +45,106 @@ namespace Avalonia.Diagnostics.ViewModels
public override void Dispose() public override void Dispose()
{ {
_subscription?.Dispose(); _subscriptions.Dispose();
}
private static IObservable<PopupRoot?>? GetHostedPopupRootObservable(IVisual visual)
{
static IObservable<PopupRoot?> GetPopupHostObservable(
IPopupHostProvider popupHostProvider,
string? providerName = null)
{
return Observable.FromEvent<IPopupHost?>(
x => popupHostProvider.PopupHostChanged += x,
x => popupHostProvider.PopupHostChanged -= x)
.StartWith(popupHostProvider.PopupHost)
.Select(popupHost =>
{
if (popupHost is IControl control)
return new PopupRoot(
control,
providerName != null ? $"{providerName} ({control.GetType().Name})" : null);
return (PopupRoot?)null;
});
}
return visual switch
{
Popup p => GetPopupHostObservable(p),
Control c => Observable.CombineLatest(
c.GetObservable(Control.ContextFlyoutProperty),
c.GetObservable(Control.ContextMenuProperty),
c.GetObservable(FlyoutBase.AttachedFlyoutProperty),
c.GetObservable(ToolTipDiagnostics.ToolTipProperty),
(ContextFlyout, ContextMenu, AttachedFlyout, ToolTip) =>
{
if (ContextMenu != null)
//Note: ContextMenus are special since all the items are added as visual children.
//So we don't need to go via Popup
return Observable.Return<PopupRoot?>(new PopupRoot(ContextMenu));
if (ContextFlyout != null)
return GetPopupHostObservable(ContextFlyout, "ContextFlyout");
if (AttachedFlyout != null)
return GetPopupHostObservable(AttachedFlyout, "AttachedFlyout");
if (ToolTip != null)
return GetPopupHostObservable(ToolTip, "ToolTip");
return Observable.Return<PopupRoot?>(null);
})
.Switch(),
_ => null
};
} }
protected override void Initialize(AvaloniaList<TreeNode> nodes) protected override void Initialize(AvaloniaList<TreeNode> nodes)
{ {
_subscription = _control.VisualChildren.ForEachItem( _subscriptions.Clear();
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
(i, item) => nodes.RemoveAt(i), if (GetHostedPopupRootObservable(_control) is { } popupRootObservable)
() => nodes.Clear()); {
VisualTreeNode? childNode = null;
_subscriptions.Add(
popupRootObservable
.Subscribe(popupRoot =>
{
if (popupRoot != null)
{
childNode = new VisualTreeNode(
popupRoot.Value.Root,
Owner,
popupRoot.Value.CustomName);
nodes.Add(childNode);
}
else if (childNode != null)
{
nodes.Remove(childNode);
}
}));
}
_subscriptions.Add(
_control.VisualChildren.ForEachItem(
(i, item) => nodes.Insert(i, new VisualTreeNode(item, Owner)),
(i, item) => nodes.RemoveAt(i),
() => nodes.Clear()));
}
private struct PopupRoot
{
public PopupRoot(IControl root, string? customName = null)
{
Root = root;
CustomName = customName;
}
public IControl Root { get; }
public string? CustomName { get; }
} }
} }
} }

56
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -5,14 +5,14 @@
<Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto"> <Grid Name="rootGrid" RowDefinitions="Auto,Auto,*,Auto,0,Auto">
<Menu> <Menu>
<MenuItem Header="_File"> <MenuItem Header="_File">
<MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}"/> <MenuItem Header="E_xit" Command="{Binding $parent[Window].Close}" />
</MenuItem> </MenuItem>
<MenuItem Header="_View"> <MenuItem Header="_View">
<MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}"> <MenuItem Header="_Console" Command="{Binding $parent[UserControl].ToggleConsole}">
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding Console.IsVisible}" IsChecked="{Binding Console.IsVisible}"
IsEnabled="False"/> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="_Dettails"> <MenuItem Header="_Dettails">
@ -31,58 +31,68 @@
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeMarginPadding}" IsChecked="{Binding ShouldVisualizeMarginPadding}"
IsEnabled="False"/> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Visualize dirty rects" Command="{Binding ToggleVisualizeDirtyRects}"> <MenuItem Header="Visualize dirty rects" Command="{Binding ToggleVisualizeDirtyRects}">
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeDirtyRects}" IsChecked="{Binding ShouldVisualizeDirtyRects}"
IsEnabled="False"/> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}"> <MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}">
<MenuItem.Icon> <MenuItem.Icon>
<CheckBox BorderThickness="0" <CheckBox BorderThickness="0"
IsChecked="{Binding ShowFpsOverlay}" IsChecked="{Binding ShowFpsOverlay}"
IsEnabled="False"/> IsEnabled="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>
</Menu> </Menu>
<TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}"> <TabStrip Grid.Row="1" SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/> <TabStripItem Content="Logical Tree" />
<TabStripItem Content="Visual Tree"/> <TabStripItem Content="Visual Tree" />
<TabStripItem Content="Events"/> <TabStripItem Content="Events" />
</TabStrip> </TabStrip>
<ContentControl Grid.Row="2" <ContentControl Grid.Row="2"
BorderBrush="{DynamicResource ThemeControlMidBrush}" BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0" BorderThickness="0,1,0,0"
Content="{Binding Content}"/> Content="{Binding Content}" />
<GridSplitter Name="consoleSplitter" Grid.Row="3" Height="1" <GridSplitter Name="consoleSplitter" Grid.Row="3" Height="1"
Background="{DynamicResource ThemeControlMidBrush}" Background="{DynamicResource ThemeControlMidBrush}"
IsVisible="False"/> IsVisible="False" />
<views:ConsoleView Name="console" <views:ConsoleView Name="console"
Grid.Row="4" Grid.Row="4"
DataContext="{Binding Console}" DataContext="{Binding Console}"
IsVisible="{Binding IsVisible}"/> IsVisible="{Binding IsVisible}" />
<Border Grid.Row="5" <Border Grid.Row="5"
BorderBrush="{DynamicResource ThemeControlMidBrush}" BorderBrush="{DynamicResource ThemeControlMidBrush}"
BorderThickness="0,1,0,0"> BorderThickness="0,1,0,0">
<StackPanel Spacing="4" Orientation="Horizontal"> <Grid ColumnDefinitions="*, Auto">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock> <StackPanel Grid.Column="0" Spacing="4" Orientation="Horizontal">
<Separator Width="8"/> <TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<TextBlock>Focused:</TextBlock> <Separator Width="8" />
<TextBlock Text="{Binding FocusedControl}"/> <TextBlock>Focused:</TextBlock>
<Separator Width="8"/> <TextBlock Text="{Binding FocusedControl}" />
<TextBlock>Pointer Over:</TextBlock> <Separator Width="8" />
<TextBlock Text="{Binding PointerOverElement}"/> <TextBlock>Pointer Over:</TextBlock>
</StackPanel> <TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
<TextBlock Grid.Column="1"
Foreground="Gray"
Margin="2 0"
Text="Popups frozen"
IsVisible="{Binding FreezePopups}" />
</Grid>
</Border> </Border>
</Grid> </Grid>
</UserControl> </UserControl>

165
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@ -1,8 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Diagnostics.ViewModels; using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
@ -15,6 +18,7 @@ namespace Avalonia.Diagnostics.Views
internal class MainWindow : Window, IStyleHost internal class MainWindow : Window, IStyleHost
{ {
private readonly IDisposable _keySubscription; private readonly IDisposable _keySubscription;
private readonly Dictionary<Popup, IDisposable> _frozenPopupStates;
private TopLevel? _root; private TopLevel? _root;
public MainWindow() public MainWindow()
@ -23,23 +27,26 @@ namespace Avalonia.Diagnostics.Views
_keySubscription = InputManager.Instance.Process _keySubscription = InputManager.Instance.Process
.OfType<RawKeyEventArgs>() .OfType<RawKeyEventArgs>()
.Where(x => x.Type == RawKeyEventType.KeyDown)
.Subscribe(RawKeyDown); .Subscribe(RawKeyDown);
_frozenPopupStates = new Dictionary<Popup, IDisposable>();
EventHandler? lh = default; EventHandler? lh = default;
lh = (s, e) => lh = (s, e) =>
{ {
this.Opened -= lh; this.Opened -= lh;
if ((DataContext as MainViewModel)?.StartupScreenIndex is int index) if ((DataContext as MainViewModel)?.StartupScreenIndex is { } index)
{ {
var screens = this.Screens; var screens = this.Screens;
if (index > -1 && index < screens.ScreenCount) if (index > -1 && index < screens.ScreenCount)
{ {
var screen = screens.All[index]; var screen = screens.All[index];
this.Position = screen.Bounds.TopLeft; this.Position = screen.Bounds.TopLeft;
this.WindowState = WindowState.Maximized; this.WindowState = WindowState.Maximized;
} }
} }
}; };
this.Opened += lh; this.Opened += lh;
} }
@ -77,6 +84,13 @@ namespace Avalonia.Diagnostics.Views
base.OnClosed(e); base.OnClosed(e);
_keySubscription.Dispose(); _keySubscription.Dispose();
foreach (var state in _frozenPopupStates)
{
state.Value.Dispose();
}
_frozenPopupStates.Clear();
if (_root != null) if (_root != null)
{ {
_root.Closed -= RootClosed; _root.Closed -= RootClosed;
@ -91,6 +105,53 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private IControl? GetHoveredControl(TopLevel topLevel)
{
#pragma warning disable CS0618 // Type or member is obsolete
var point = (topLevel as IInputRoot)?.MouseDevice?.GetPosition(topLevel) ?? default;
#pragma warning restore CS0618 // Type or member is obsolete
return (IControl?)topLevel.GetVisualsAt(point, x =>
{
if (x is AdornerLayer || !x.IsVisible)
{
return false;
}
return !(x is IInputElement ie) || ie.IsHitTestVisible;
})
.FirstOrDefault();
}
private static List<PopupRoot> GetPopupRoots(IVisual root)
{
var popupRoots = new List<PopupRoot>();
void ProcessProperty<T>(IControl control, AvaloniaProperty<T> property)
{
if (control.GetValue(property) is IPopupHostProvider popupProvider
&& popupProvider.PopupHost is PopupRoot popupRoot)
{
popupRoots.Add(popupRoot);
}
}
foreach (var control in root.GetVisualDescendants().OfType<IControl>())
{
if (control is Popup p && p.Host is PopupRoot popupRoot)
{
popupRoots.Add(popupRoot);
}
ProcessProperty(control, ContextFlyoutProperty);
ProcessProperty(control, ContextMenuProperty);
ProcessProperty(control, FlyoutBase.AttachedFlyoutProperty);
ProcessProperty(control, ToolTipDiagnostics.ToolTipProperty);
}
return popupRoots;
}
private void RawKeyDown(RawKeyEventArgs e) private void RawKeyDown(RawKeyEventArgs e)
{ {
var vm = (MainViewModel?)DataContext; var vm = (MainViewModel?)DataContext;
@ -99,34 +160,72 @@ namespace Avalonia.Diagnostics.Views
return; return;
} }
const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; switch (e.Modifiers)
if (e.Modifiers == modifiers)
{ {
#pragma warning disable CS0618 // Type or member is obsolete case RawInputModifiers.Control | RawInputModifiers.Shift:
var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; {
#pragma warning restore CS0618 // Type or member is obsolete IControl? control = null;
foreach (var popupRoot in GetPopupRoots(Root))
{
control = GetHoveredControl(popupRoot);
if (control != null)
{
break;
}
}
control ??= GetHoveredControl(Root);
var control = Root.GetVisualsAt(point, x => if (control != null)
{ {
if (x is AdornerLayer || !x.IsVisible) return false; vm.SelectControl(control);
if (!(x is IInputElement ie)) return true; }
return ie.IsHitTestVisible;
}) break;
.FirstOrDefault(); }
if (control != null) case RawInputModifiers.Control | RawInputModifiers.Alt when e.Key == Key.F:
{ {
vm.SelectControl((IControl)control); vm.FreezePopups = !vm.FreezePopups;
foreach (var popupRoot in GetPopupRoots(Root))
{
if (popupRoot.Parent is Popup popup)
{
if (vm.FreezePopups)
{
var lightDismissEnabledState = popup.SetValue(
Popup.IsLightDismissEnabledProperty,
!vm.FreezePopups,
BindingPriority.Animation);
if (lightDismissEnabledState != null)
{
_frozenPopupStates[popup] = lightDismissEnabledState;
}
}
else
{
//TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
if (_frozenPopupStates.ContainsKey(popup))
{
_frozenPopupStates[popup].Dispose();
_frozenPopupStates.Remove(popup);
}
}
}
}
break;
} }
}
else if (e.Modifiers == RawInputModifiers.Alt) case RawInputModifiers.Alt when e.Key == Key.S || e.Key == Key.D:
{
if (e.Key == Key.S || e.Key == Key.D)
{ {
var enable = e.Key == Key.S; vm.EnableSnapshotStyles(e.Key == Key.S);
vm.EnableSnapshotStyles(enable); break;
} }
} }
} }

2
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml

@ -11,7 +11,7 @@
<TreeDataTemplate DataType="vm:TreeNode" <TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}"> ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{Binding Type}"/> <TextBlock Text="{Binding Type}" FontWeight="{Binding FontWeight}"/>
<TextBlock Text="{Binding Classes}"/> <TextBlock Text="{Binding Classes}"/>
<TextBlock Foreground="Gray" Text="{Binding ElementName}"/> <TextBlock Foreground="Gray" Text="{Binding ElementName}"/>
</StackPanel> </StackPanel>

13
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -47,7 +47,7 @@ namespace Avalonia.Headless
public IEnumerable<object> Surfaces { get; } public IEnumerable<object> Surfaces { get; }
public Action<RawInputEventArgs> Input { get; set; } public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; } public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; } public Action<Size, PlatformResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; } public Action<double> ScalingChanged { get; set; }
public IRenderer CreateRenderer(IRenderRoot root) public IRenderer CreateRenderer(IRenderRoot root)
@ -76,7 +76,7 @@ namespace Avalonia.Headless
public Action Closed { get; set; } public Action Closed { get; set; }
public IMouseDevice MouseDevice { get; } public IMouseDevice MouseDevice { get; }
public void Show(bool activate) public void Show(bool activate, bool isDialog)
{ {
if (activate) if (activate)
Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input);
@ -108,7 +108,7 @@ namespace Avalonia.Headless
public Action Activated { get; set; } public Action Activated { get; set; }
public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB");
public Size MaxClientSize { get; } = new Size(1920, 1280); public Size MaxClientSize { get; } = new Size(1920, 1280);
public void Resize(Size clientSize) public void Resize(Size clientSize, PlatformResizeReason reason)
{ {
// Emulate X11 behavior here // Emulate X11 behavior here
if (IsPopup) if (IsPopup)
@ -126,7 +126,7 @@ namespace Avalonia.Headless
if (ClientSize != clientSize) if (ClientSize != clientSize)
{ {
ClientSize = clientSize; ClientSize = clientSize;
Resized?.Invoke(clientSize); Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified);
} }
} }
@ -148,11 +148,6 @@ namespace Avalonia.Headless
} }
public void ShowDialog(IWindowImpl parent)
{
Show(true);
}
public void SetSystemDecorations(bool enabled) public void SetSystemDecorations(bool enabled)
{ {

3
src/Avalonia.Input/Avalonia.Input.csproj

@ -4,6 +4,9 @@
<Nullable>Enable</Nullable> <Nullable>Enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors> <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" /> <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

2
src/Avalonia.Input/Cursor.cs

@ -37,7 +37,7 @@ namespace Avalonia.Input
BottomSize = BottomSide BottomSize = BottomSide
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
// We might enable them later, preferably, by loading pixmax direclty from theme with fallback image // We might enable them later, preferably, by loading pixmax directly from theme with fallback image
// SizeNorthWestSouthEast, // SizeNorthWestSouthEast,
// SizeNorthEastSouthWest, // SizeNorthEastSouthWest,
} }

13
src/Avalonia.Input/FocusManager.cs

@ -92,6 +92,17 @@ namespace Avalonia.Input
} }
} }
public IInputElement? GetFocusedElement(IInputElement e)
{
if (e is IFocusScope scope)
{
_focusScopes.TryGetValue(scope, out var result);
return result;
}
return null;
}
/// <summary> /// <summary>
/// Sets the currently focused element in the specified scope. /// Sets the currently focused element in the specified scope.
/// </summary> /// </summary>
@ -151,6 +162,8 @@ namespace Avalonia.Input
Focus(e); Focus(e);
} }
public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
/// <summary> /// <summary>
/// Checks if the specified element can be focused. /// Checks if the specified element can be focused.
/// </summary> /// </summary>

2
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -78,7 +78,7 @@ namespace Avalonia.Input.GestureRecognizers
// Arbitrary chosen value, probably need to move that to platform settings or something // Arbitrary chosen value, probably need to move that to platform settings or something
private const double ScrollStartDistance = 30; private const double ScrollStartDistance = 30;
// Pixels per second speed that is considered to be the stop of inertiall scroll // Pixels per second speed that is considered to be the stop of inertial scroll
private const double InertialScrollSpeedEnd = 5; private const double InertialScrollSpeedEnd = 5;
public void PointerMoved(PointerEventArgs e) public void PointerMoved(PointerEventArgs e)

2
src/Avalonia.Input/ICommandSource.cs

@ -22,7 +22,7 @@ namespace Avalonia.Input
/// <summary> /// <summary>
/// Bor the bheavior CanExecuteChanged /// Bor the behavior CanExecuteChanged
/// </summary> /// </summary>
/// <param name="sender"></param> /// <param name="sender"></param>
/// <param name="e"></param> /// <param name="e"></param>

17
src/Avalonia.Input/ICustomKeyboardNavigation.cs

@ -1,4 +1,5 @@
 #nullable enable
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
@ -6,6 +7,18 @@ namespace Avalonia.Input
/// </summary> /// </summary>
public interface ICustomKeyboardNavigation public interface ICustomKeyboardNavigation
{ {
(bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction); /// <summary>
/// Gets the next element in the specified navigation direction.
/// </summary>
/// <param name="element">The element being navigated from.</param>
/// <param name="direction">The navigation direction.</param>
/// <returns>
/// A tuple consisting of:
/// - A boolean indicating whether the request was handled. If false is returned then
/// custom navigation will be ignored and default navigation will take place.
/// - If handled is true: the next element in the navigation direction, or null if default
/// navigation should continue outside the element.
/// </returns>
(bool handled, IInputElement? next) GetNext(IInputElement element, NavigationDirection direction);
} }
} }

31
src/Avalonia.Input/InputElement.cs

@ -71,6 +71,12 @@ namespace Avalonia.Input
public static readonly DirectProperty<InputElement, bool> IsPointerOverProperty = public static readonly DirectProperty<InputElement, bool> IsPointerOverProperty =
AvaloniaProperty.RegisterDirect<InputElement, bool>(nameof(IsPointerOver), o => o.IsPointerOver); AvaloniaProperty.RegisterDirect<InputElement, bool>(nameof(IsPointerOver), o => o.IsPointerOver);
/// <summary>
/// Defines the <see cref="IsTabStop"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsTabStopProperty =
KeyboardNavigation.IsTabStopProperty.AddOwner<InputElement>();
/// <summary> /// <summary>
/// Defines the <see cref="GotFocus"/> event. /// Defines the <see cref="GotFocus"/> event.
/// </summary> /// </summary>
@ -99,6 +105,12 @@ namespace Avalonia.Input
"KeyUp", "KeyUp",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble); RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="TabIndex"/> property.
/// </summary>
public static readonly StyledProperty<int> TabIndexProperty =
KeyboardNavigation.TabIndexProperty.AddOwner<InputElement>();
/// <summary> /// <summary>
/// Defines the <see cref="TextInput"/> event. /// Defines the <see cref="TextInput"/> event.
/// </summary> /// </summary>
@ -426,6 +438,15 @@ namespace Avalonia.Input
internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); }
} }
/// <summary>
/// Gets or sets a value that indicates whether the control is included in tab navigation.
/// </summary>
public bool IsTabStop
{
get => GetValue(IsTabStopProperty);
set => SetValue(IsTabStopProperty, value);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsEffectivelyEnabled public bool IsEffectivelyEnabled
{ {
@ -437,6 +458,16 @@ namespace Avalonia.Input
} }
} }
/// <summary>
/// Gets or sets a value that determines the order in which elements receive focus when the
/// user navigates through controls by pressing the Tab key.
/// </summary>
public int TabIndex
{
get => GetValue(TabIndexProperty);
set => SetValue(TabIndexProperty, value);
}
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>(); public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
/// <summary> /// <summary>

33
src/Avalonia.Input/KeyboardNavigation.cs

@ -5,6 +5,15 @@ namespace Avalonia.Input
/// </summary> /// </summary>
public static class KeyboardNavigation public static class KeyboardNavigation
{ {
/// <summary>
/// Defines the TabIndex attached property.
/// </summary>
public static readonly AttachedProperty<int> TabIndexProperty =
AvaloniaProperty.RegisterAttached<StyledElement, int>(
"TabIndex",
typeof(KeyboardNavigation),
int.MaxValue);
/// <summary> /// <summary>
/// Defines the TabNavigation attached property. /// Defines the TabNavigation attached property.
/// </summary> /// </summary>
@ -42,6 +51,26 @@ namespace Avalonia.Input
typeof(KeyboardNavigation), typeof(KeyboardNavigation),
true); true);
/// <summary>
/// Gets the <see cref="TabIndexProperty"/> for an element.
/// </summary>
/// <param name="element">The container.</param>
/// <returns>The <see cref="KeyboardNavigationMode"/> for the container.</returns>
public static int GetTabIndex(IInputElement element)
{
return ((IAvaloniaObject)element).GetValue(TabIndexProperty);
}
/// <summary>
/// Sets the <see cref="TabIndexProperty"/> for an element.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="value">The tab index.</param>
public static void SetTabIndex(IInputElement element, int value)
{
((IAvaloniaObject)element).SetValue(TabIndexProperty, value);
}
/// <summary> /// <summary>
/// Gets the <see cref="TabNavigationProperty"/> for a container. /// Gets the <see cref="TabNavigationProperty"/> for a container.
/// </summary> /// </summary>
@ -83,7 +112,7 @@ namespace Avalonia.Input
} }
/// <summary> /// <summary>
/// Sets the <see cref="IsTabStopProperty"/> for a container. /// Sets the <see cref="IsTabStopProperty"/> for an element.
/// </summary> /// </summary>
/// <param name="element">The container.</param> /// <param name="element">The container.</param>
/// <param name="value">Value indicating whether the container is a tab stop.</param> /// <param name="value">Value indicating whether the container is a tab stop.</param>
@ -93,7 +122,7 @@ namespace Avalonia.Input
} }
/// <summary> /// <summary>
/// Gets the <see cref="IsTabStopProperty"/> for a container. /// Gets the <see cref="IsTabStopProperty"/> for an element.
/// </summary> /// </summary>
/// <param name="element">The container.</param> /// <param name="element">The container.</param>
/// <returns>Whether the container is a tab stop.</returns> /// <returns>Whether the container is a tab stop.</returns>

111
src/Avalonia.Input/KeyboardNavigationHandler.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Input.Navigation; using Avalonia.Input.Navigation;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -48,39 +49,24 @@ namespace Avalonia.Input
{ {
element = element ?? throw new ArgumentNullException(nameof(element)); element = element ?? throw new ArgumentNullException(nameof(element));
var customHandler = element.GetSelfAndVisualAncestors() // If there's a custom keyboard navigation handler as an ancestor, use that.
.OfType<ICustomKeyboardNavigation>() var custom = element.FindAncestorOfType<ICustomKeyboardNavigation>(true);
.FirstOrDefault(); if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce))
return ce;
if (customHandler != null) var result = direction switch
{ {
var (handled, next) = customHandler.GetNext(element, direction); NavigationDirection.Next => TabNavigation.GetNextTab(element, false),
NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false),
_ => throw new NotSupportedException(),
};
if (handled) // If there wasn't a custom navigation handler as an ancestor of the current element,
{ // but there is one as an ancestor of the new element, use that.
if (next != null) if (custom is null && HandlePostCustomNavigation(element, result, direction, out ce))
{ return ce;
return next;
}
else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true);
}
else
{
return null;
}
}
}
if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) return result;
{
return TabNavigation.GetNextInTabOrder(element, direction);
}
else
{
throw new NotSupportedException();
}
} }
/// <summary> /// <summary>
@ -90,7 +76,7 @@ namespace Avalonia.Input
/// <param name="direction">The direction to move.</param> /// <param name="direction">The direction to move.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param> /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Move( public void Move(
IInputElement element, IInputElement element,
NavigationDirection direction, NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None) KeyModifiers keyModifiers = KeyModifiers.None)
{ {
@ -124,5 +110,70 @@ namespace Avalonia.Input
e.Handled = true; e.Handled = true;
} }
} }
private static bool HandlePreCustomNavigation(
ICustomKeyboardNavigation customHandler,
IInputElement element,
NavigationDirection direction,
[NotNullWhen(true)] out IInputElement? result)
{
if (customHandler != null)
{
var (handled, next) = customHandler.GetNext(element, direction);
if (handled)
{
if (next != null)
{
result = next;
return true;
}
else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
var r = direction switch
{
NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
_ => throw new NotSupportedException(),
};
if (r is object)
{
result = r;
return true;
}
}
}
}
result = null;
return false;
}
private static bool HandlePostCustomNavigation(
IInputElement element,
IInputElement? newElement,
NavigationDirection direction,
[NotNullWhen(true)] out IInputElement? result)
{
if (newElement is object)
{
var customHandler = newElement.FindAncestorOfType<ICustomKeyboardNavigation>(true);
if (customHandler is object)
{
var (handled, next) = customHandler.GetNext(element, direction);
if (handled && next is object)
{
result = next;
return true;
}
}
}
result = null;
return false;
}
} }
} }

7
src/Avalonia.Input/KeyboardNavigationMode.cs

@ -36,5 +36,10 @@ namespace Avalonia.Input
/// The container's children will not be focused when using the tab key. /// The container's children will not be focused when using the tab key.
/// </summary> /// </summary>
None, None,
/// <summary>
/// TabIndexes are considered on local subtree only inside this container
/// </summary>
Local,
} }
} }

734
src/Avalonia.Input/Navigation/TabNavigation.cs

@ -10,277 +10,663 @@ namespace Avalonia.Input.Navigation
/// </summary> /// </summary>
internal static class TabNavigation internal static class TabNavigation
{ {
/// <summary> public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly)
/// Gets the next control in the specified tab direction.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
/// <param name="outsideElement">
/// If true will not descend into <paramref name="element"/> to find next control.
/// </param>
/// <returns>
/// The next element in the specified direction, or null if <paramref name="element"/>
/// was the last in the requested direction.
/// </returns>
public static IInputElement? GetNextInTabOrder(
IInputElement element,
NavigationDirection direction,
bool outsideElement = false)
{ {
element = element ?? throw new ArgumentNullException(nameof(element)); return GetNextTab(e, GetGroupParent(e), goDownOnly);
}
if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous)
{
throw new ArgumentException("Invalid direction: must be Next or Previous.");
}
var container = element.GetVisualParent<IInputElement>(); public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly)
{
var tabbingType = GetKeyNavigationMode(container);
if (container != null) if (e == null)
{ {
var mode = KeyboardNavigation.GetTabNavigation((InputElement)container); if (IsTabStop(container))
return container;
switch (mode) // Using ActiveElement if set
var activeElement = GetActiveElement(container);
if (activeElement != null)
return GetNextTab(null, activeElement, true);
}
else
{
if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
{ {
case KeyboardNavigationMode.Continue: if (container != e)
return GetNextInContainer(element, container, direction, outsideElement) ?? {
GetFirstInNextContainer(element, element, direction); if (goDownOnly)
case KeyboardNavigationMode.Cycle: return null;
return GetNextInContainer(element, container, direction, outsideElement) ?? var parentContainer = GetGroupParent(container);
GetFocusableDescendant(container, direction); return GetNextTab(container, parentContainer, goDownOnly);
case KeyboardNavigationMode.Contained: }
return GetNextInContainer(element, container, direction, outsideElement);
default:
return GetFirstInNextContainer(element, container, direction);
} }
} }
else
// All groups
IInputElement? loopStartElement = null;
var nextTabElement = e;
var currentTabbingType = tabbingType;
// Search down inside the container
while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null)
{ {
return GetFocusableDescendants(element, direction).FirstOrDefault(); // Avoid the endless loop here for Cycle groups
if (loopStartElement == nextTabElement)
break;
if (loopStartElement == null)
loopStartElement = nextTabElement;
var firstTabElementInside = GetNextTab(null, nextTabElement, true);
if (firstTabElementInside != null)
return firstTabElementInside;
// If we want to continue searching inside the Once groups, we should change the navigation mode
if (currentTabbingType == KeyboardNavigationMode.Once)
currentTabbingType = KeyboardNavigationMode.Contained;
} }
// If there is no next element in the group (nextTabElement == null)
// Search up in the tree if allowed
// consider: Use original tabbingType instead of currentTabbingType
if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null)
{
return GetNextTab(container, GetGroupParent(container), false);
}
return null;
} }
/// <summary> public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e)
/// Gets the first or last focusable descendant of the specified element.
/// </summary>
/// <param name="container">The element.</param>
/// <param name="direction">The direction to search.</param>
/// <returns>The element or null if not found.##</returns>
private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
{ {
return direction == NavigationDirection.Next ? if (e is IInputElement container)
GetFocusableDescendants(container, direction).FirstOrDefault() : {
GetFocusableDescendants(container, direction).LastOrDefault(); var last = GetLastInTree(container);
if (last is object)
return GetNextTab(last, false);
}
return null;
} }
/// <summary> public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly)
/// Gets the focusable descendants of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <param name="direction">The tab direction. Must be Next or Previous.</param>
/// <returns>The element's focusable descendants.</returns>
private static IEnumerable<IInputElement> GetFocusableDescendants(IInputElement element,
NavigationDirection direction)
{ {
var mode = KeyboardNavigation.GetTabNavigation((InputElement)element); if (e is null && container is null)
throw new InvalidOperationException("Either 'e' or 'container' must be non-null.");
if (mode == KeyboardNavigationMode.None) if (container is null)
{ container = GetGroupParent(e!);
yield break;
}
var children = element.GetVisualChildren().OfType<IInputElement>(); KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container);
if (mode == KeyboardNavigationMode.Once) if (e == null)
{ {
var active = KeyboardNavigation.GetTabOnceActiveElement((InputElement)element); // Using ActiveElement if set
var activeElement = GetActiveElement(container);
if (active != null) if (activeElement != null)
return GetPrevTab(null, activeElement, true);
else
{ {
yield return active; // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null
yield break; // then we want to go to the first item (not last) within the container
if (tabbingType == KeyboardNavigationMode.Once)
{
var firstTabElement = GetNextTabInGroup(null, container, tabbingType);
if (firstTabElement == null)
{
if (IsTabStop(container))
return container;
if (goDownOnly)
return null;
return GetPrevTab(container, null, false);
}
else
{
return GetPrevTab(null, firstTabElement, true);
}
}
} }
else }
else
{
if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
{ {
children = children.Take(1); if (goDownOnly || container == e)
return null;
// FocusedElement should not be e otherwise we will delegate focus to the same element
if (IsTabStop(container))
return container;
return GetPrevTab(container, null, false);
} }
} }
foreach (var child in children) // All groups (except Once) - continue
IInputElement? loopStartElement = null;
IInputElement? nextTabElement = e;
// Look for element with the same TabIndex before the current element
while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null)
{ {
var customNext = GetCustomNext(child, direction); if (nextTabElement == container && tabbingType == KeyboardNavigationMode.Local)
break;
// At this point nextTabElement is TabStop or TabGroup
// In case it is a TabStop only return the element
if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement))
return nextTabElement;
// Avoid the endless loop here
if (loopStartElement == nextTabElement)
break;
if (loopStartElement == null)
loopStartElement = nextTabElement;
// At this point nextTabElement is TabGroup
var lastTabElementInside = GetPrevTab(null, nextTabElement, true);
if (lastTabElementInside != null)
return lastTabElementInside;
}
if (customNext.handled) if (tabbingType == KeyboardNavigationMode.Contained)
{ return null;
yield return customNext.next!;
} if (e != container && IsTabStop(container))
else return container;
// If end of the subtree is reached or there no other elements above
if (!goDownOnly && GetParent(container) != null)
{
return GetPrevTab(container, null, false);
}
return null;
}
public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e)
{
if (e is IInputElement container)
{
var first = GetFirstChild(container);
if (first is object)
return GetPrevTab(first, null, false);
}
return null;
}
private static IInputElement? FocusedElement(IInputElement e)
{
var iie = e;
// Focus delegation is enabled only if keyboard focus is outside the container
if (iie != null && !iie.IsKeyboardFocusWithin)
{
var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e);
if (focusedElement != null)
{ {
if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child)) if (!IsFocusScope(e))
{ {
yield return child; // Verify if focusedElement is a visual descendant of e
if (focusedElement is IVisual visualFocusedElement &&
visualFocusedElement != e &&
e.IsVisualAncestorOf(visualFocusedElement))
{
return focusedElement;
}
} }
}
}
return null;
}
private static IInputElement? GetFirstChild(IInputElement e)
{
// If the element has a FocusedElement it should be its first child
if (FocusedElement(e) is IInputElement focusedElement)
return focusedElement;
if (child.CanFocusDescendants()) // Return the first visible element.
var uiElement = e as InputElement;
if (uiElement is null || uiElement.IsVisible)
{
if (e is IVisual elementAsVisual)
{
var children = elementAsVisual.VisualChildren;
var count = children.Count;
for (int i = 0; i < count; i++)
{ {
foreach (var descendant in GetFocusableDescendants(child, direction)) if (children[i] is InputElement ie)
{ {
if (KeyboardNavigation.GetIsTabStop((InputElement)descendant)) if (ie.IsVisible)
return ie;
else
{ {
yield return descendant; var firstChild = GetFirstChild(ie);
if (firstChild != null)
return firstChild;
} }
} }
} }
} }
} }
return null;
} }
/// <summary> private static IInputElement? GetLastChild(IInputElement e)
/// Gets the next item that should be focused in the specified container.
/// </summary>
/// <param name="element">The starting element/</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction.</param>
/// <param name="outsideElement">
/// If true will not descend into <paramref name="element"/> to find next control.
/// </param>
/// <returns>The next element, or null if the element is the last.</returns>
private static IInputElement? GetNextInContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction,
bool outsideElement)
{ {
IInputElement? e = element; // If the element has a FocusedElement it should be its last child
if (FocusedElement(e) is IInputElement focusedElement)
return focusedElement;
if (direction == NavigationDirection.Next && !outsideElement) // Return the last visible element.
{ var uiElement = e as InputElement;
var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
if (descendant != null) if (uiElement == null || uiElement.IsVisible)
{
return descendant;
}
}
if (container != null)
{ {
var navigable = container as INavigableContainer; var elementAsVisual = e as IVisual;
// TODO: Do a spatial search here if the container doesn't implement if (elementAsVisual != null)
// INavigableContainer.
if (navigable != null)
{ {
while (e != null) var children = elementAsVisual.VisualChildren;
{ var count = children.Count;
e = navigable.GetControl(direction, e, false);
if (e != null && for (int i = count - 1; i >= 0; i--)
e.CanFocus() && {
KeyboardNavigation.GetIsTabStop((InputElement)e)) if (children[i] is InputElement ie)
{ {
break; if (ie.IsVisible)
return ie;
else
{
var lastChild = GetLastChild(ie);
if (lastChild != null)
return lastChild;
}
} }
} }
} }
else }
return null;
}
private static IInputElement? GetFirstTabInGroup(IInputElement container)
{
IInputElement? firstTabElement = null;
int minIndexFirstTab = int.MinValue;
var currElement = container;
while ((currElement = GetNextInTree(currElement, container)) != null)
{
if (IsTabStopOrGroup(currElement))
{ {
// TODO: Do a spatial search here if the container doesn't implement int currPriority = KeyboardNavigation.GetTabIndex(currElement);
// INavigableContainer.
e = null; if (currPriority < minIndexFirstTab || firstTabElement == null)
{
minIndexFirstTab = currPriority;
firstTabElement = currElement;
}
} }
}
return firstTabElement;
}
private static IInputElement? GetLastInTree(IInputElement container)
{
IInputElement? result;
IInputElement? c = container;
do
{
result = c;
c = GetLastChild(c);
} while (c != null && !IsGroup(c));
if (c != null)
return c;
return result;
}
if (e != null && direction == NavigationDirection.Previous) private static IInputElement? GetLastTabInGroup(IInputElement container)
{
IInputElement? lastTabElement = null;
int maxIndexFirstTab = int.MaxValue;
var currElement = GetLastInTree(container);
while (currElement != null && currElement != container)
{
if (IsTabStopOrGroup(currElement))
{ {
var descendant = GetFocusableDescendants(e, direction).LastOrDefault(); int currPriority = KeyboardNavigation.GetTabIndex(currElement);
if (descendant != null) if (currPriority > maxIndexFirstTab || lastTabElement == null)
{ {
return descendant; maxIndexFirstTab = currPriority;
lastTabElement = currElement;
} }
} }
currElement = GetPreviousInTree(currElement, container);
}
return lastTabElement;
}
private static IInputElement? GetNextInTree(IInputElement e, IInputElement container)
{
IInputElement? result = null;
if (e == container || !IsGroup(e))
result = GetFirstChild(e);
if (result != null || e == container)
return result;
IInputElement? parent = e;
do
{
var sibling = GetNextSibling(parent);
if (sibling != null)
return sibling;
return e; parent = GetParent(parent);
} while (parent != null && parent != container);
return null;
}
private static IInputElement? GetNextSibling(IInputElement e)
{
if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual)
{
var children = parentAsVisual.VisualChildren;
var count = children.Count;
var i = 0;
//go till itself
for (; i < count; i++)
{
var vchild = children[i];
if (vchild == elementAsVisual)
break;
}
i++;
//search ahead
for (; i < count; i++)
{
var visual = children[i];
if (visual is IInputElement ie)
return ie;
}
} }
return null; return null;
} }
/// <summary> private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
/// Gets the first item that should be focused in the next container.
/// </summary>
/// <param name="element">The element being navigated away from.</param>
/// <param name="container">The container.</param>
/// <param name="direction">The direction of the search.</param>
/// <returns>The first element, or null if there are no more elements.</returns>
private static IInputElement? GetFirstInNextContainer(
IInputElement element,
IInputElement container,
NavigationDirection direction)
{ {
var parent = container.GetVisualParent<IInputElement>(); // None groups: Tab navigation is not supported
IInputElement? next = null; if (tabbingType == KeyboardNavigationMode.None)
return null;
if (parent != null) // e == null or e == container -> return the first TabStopOrGroup
if (e == null || e == container)
{ {
if (direction == NavigationDirection.Previous && return GetFirstTabInGroup(container);
parent.CanFocus() && }
KeyboardNavigation.GetIsTabStop((InputElement) parent))
if (tabbingType == KeyboardNavigationMode.Once)
return null;
var nextTabElement = GetNextTabWithSameIndex(e, container);
if (nextTabElement != null)
return nextTabElement;
return GetNextTabWithNextIndex(e, container, tabbingType);
}
private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container)
{
var elementTabPriority = KeyboardNavigation.GetTabIndex(e);
var currElement = e;
while ((currElement = GetNextInTree(currElement, container)) != null)
{
if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority)
{ {
return parent; return currElement;
} }
}
var allSiblings = parent.GetVisualChildren() return null;
.OfType<IInputElement>() }
.Where(FocusExtensions.CanFocusDescendants);
var siblings = direction == NavigationDirection.Next ?
allSiblings.SkipWhile(x => x != container).Skip(1) :
allSiblings.TakeWhile(x => x != container).Reverse();
foreach (var sibling in siblings) private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType)
{
// Find the next min index in the tree
// min (index>currentTabIndex)
IInputElement? nextTabElement = null;
IInputElement? firstTabElement = null;
int minIndexFirstTab = int.MinValue;
int minIndex = int.MinValue;
int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
IInputElement? currElement = container;
while ((currElement = GetNextInTree(currElement, container)) != null)
{
if (IsTabStopOrGroup(currElement))
{ {
var customNext = GetCustomNext(sibling, direction); int currPriority = KeyboardNavigation.GetTabIndex(currElement);
if (customNext.handled) if (currPriority > elementTabPriority)
{ {
return customNext.next; if (currPriority < minIndex || nextTabElement == null)
{
minIndex = currPriority;
nextTabElement = currElement;
}
} }
if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling)) if (currPriority < minIndexFirstTab || firstTabElement == null)
{ {
return sibling; minIndexFirstTab = currPriority;
firstTabElement = currElement;
} }
}
}
next = direction == NavigationDirection.Next ? // Cycle groups: if not found - return first element
GetFocusableDescendants(sibling, direction).FirstOrDefault() : if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
GetFocusableDescendants(sibling, direction).LastOrDefault(); nextTabElement = firstTabElement;
return nextTabElement;
}
if (next != null) private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
{
// None groups: Tab navigation is not supported
if (tabbingType == KeyboardNavigationMode.None)
return null;
// Search the last index inside the group
if (e == null)
{
return GetLastTabInGroup(container);
}
if (tabbingType == KeyboardNavigationMode.Once)
return null;
if (e == container)
return null;
var nextTabElement = GetPrevTabWithSameIndex(e, container);
if (nextTabElement != null)
return nextTabElement;
return GetPrevTabWithPrevIndex(e, container, tabbingType);
}
private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container)
{
int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
var currElement = GetPreviousInTree(e, container);
while (currElement != null)
{
if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container)
{
return currElement;
}
currElement = GetPreviousInTree(currElement, container);
}
return null;
}
private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType)
{
// Find the next max index in the tree
// max (index<currentTabIndex)
IInputElement? lastTabElement = null;
IInputElement? nextTabElement = null;
int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
int maxIndexFirstTab = Int32.MaxValue;
int maxIndex = Int32.MaxValue;
var currElement = GetLastInTree(container);
while (currElement != null)
{
if (IsTabStopOrGroup(currElement) && currElement != container)
{
int currPriority = KeyboardNavigation.GetTabIndex(currElement);
if (currPriority < elementTabPriority)
{
if (currPriority > maxIndex || nextTabElement == null)
{
maxIndex = currPriority;
nextTabElement = currElement;
}
}
if (currPriority > maxIndexFirstTab || lastTabElement == null)
{ {
return next; maxIndexFirstTab = currPriority;
lastTabElement = currElement;
} }
} }
next = GetFirstInNextContainer(element, parent, direction); currElement = GetPreviousInTree(currElement, container);
}
// Cycle groups: if not found - return first element
if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
nextTabElement = lastTabElement;
return nextTabElement;
}
private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container)
{
if (e == container)
return null;
var result = GetPreviousSibling(e);
if (result != null)
{
if (IsGroup(result))
return result;
else
return GetLastInTree(result);
} }
else else
return GetParent(e);
}
private static IInputElement? GetPreviousSibling(IInputElement e)
{
if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual)
{ {
next = direction == NavigationDirection.Next ? var children = parentAsVisual.VisualChildren;
GetFocusableDescendants(container, direction).FirstOrDefault() : var count = children.Count;
GetFocusableDescendants(container, direction).LastOrDefault(); IInputElement? prev = null;
for (int i = 0; i < count; i++)
{
var vchild = children[i];
if (vchild == elementAsVisual)
break;
if (vchild.IsVisible == true && vchild is IInputElement ie)
prev = ie;
}
return prev;
} }
return null;
}
return next; private static IInputElement? GetActiveElement(IInputElement e)
{
return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty);
} }
private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element, private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false);
NavigationDirection direction)
private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent)
{ {
if (element is ICustomKeyboardNavigation custom) var result = element; // Keep the last non null element
var e = element;
// If we don't want to include the current element,
// start at the parent of the element. If the element
// is the root, then just return it as the group parent.
if (!includeCurrent)
{
result = e;
e = GetParent(e);
if (e == null)
return result;
}
while (e != null)
{ {
return custom.GetNext(element, direction); if (IsGroup(e))
return e;
result = e;
e = GetParent(e);
} }
return (false, null); return result;
} }
private static IInputElement? GetParent(IInputElement e)
{
// For Visual - go up the visual parent chain until we find Visual.
if (e is IVisual v)
return v.FindAncestorOfType<IInputElement>();
// This will need to be implemented when we have non-visual input elements.
throw new NotSupportedException();
}
private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e)
{
return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty);
}
private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null;
private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue;
private static bool IsTabStop(IInputElement e)
{
if (e is InputElement ie)
return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled;
return false;
}
private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e);
} }
} }

4
src/Avalonia.Input/TappedEventArgs.cs

@ -13,6 +13,10 @@ namespace Avalonia.Input
this.lastPointerEventArgs = lastPointerEventArgs; this.lastPointerEventArgs = lastPointerEventArgs;
} }
public IPointer Pointer => lastPointerEventArgs.Pointer;
public KeyModifiers KeyModifiers => lastPointerEventArgs.KeyModifiers;
public ulong Timestamp => lastPointerEventArgs.Timestamp;
public Point GetPosition(IVisual? relativeTo) => lastPointerEventArgs.GetPosition(relativeTo); public Point GetPosition(IVisual? relativeTo) => lastPointerEventArgs.GetPosition(relativeTo);
} }
} }

4
src/Avalonia.Input/TextInput/ITextInputMethodClient.cs

@ -22,11 +22,11 @@ namespace Avalonia.Input.TextInput
/// </summary> /// </summary>
event EventHandler TextViewVisualChanged; event EventHandler TextViewVisualChanged;
/// <summary> /// <summary>
/// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position /// Indicates if TextViewVisual is capable of displaying non-committed input on the cursor position
/// </summary> /// </summary>
bool SupportsPreedit { get; } bool SupportsPreedit { get; }
/// <summary> /// <summary>
/// Sets the non-commited input string /// Sets the non-committed input string
/// </summary> /// </summary>
void SetPreeditText(string text); void SetPreeditText(string text);
/// <summary> /// <summary>

2
src/Avalonia.Interactivity/Interactive.cs

@ -143,7 +143,7 @@ namespace Avalonia.Interactivity
/// <param name="e">The routed event.</param> /// <param name="e">The routed event.</param>
/// <returns>An <see cref="EventRoute"/> describing the route.</returns> /// <returns>An <see cref="EventRoute"/> describing the route.</returns>
/// <remarks> /// <remarks>
/// Usually, calling <see cref="RaiseEvent(RoutedEventArgs)"/> is sufficent to raise a routed /// Usually, calling <see cref="RaiseEvent(RoutedEventArgs)"/> is sufficient to raise a routed
/// event, however there are situations in which the construction of the event args is expensive /// event, however there are situations in which the construction of the event args is expensive
/// and should be avoided if there are no handlers for an event. In these cases you can call /// and should be avoided if there are no handlers for an event. In these cases you can call
/// this method to build the event route and check the <see cref="EventRoute.HasHandlers"/> /// this method to build the event route and check the <see cref="EventRoute.HasHandlers"/>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save