Browse Source

Merge remote-tracking branch 'origin/master' into glcontrol

# Conflicts:
#	src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
#	src/Skia/Avalonia.Skia/SkiaOptions.cs
pull/3386/head
Nikita Tsukanov 6 years ago
parent
commit
d0fdce3b60
  1. 4
      build/SkiaSharp.props
  2. 3
      native/Avalonia.Native/inc/avalonia-native.h
  3. 4
      native/Avalonia.Native/src/OSX/window.h
  4. 309
      native/Avalonia.Native/src/OSX/window.mm
  5. 14
      nukebuild/Build.cs
  6. 4
      samples/BindingDemo/MainWindow.xaml
  7. 5
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  8. 5
      samples/ControlCatalog/MainView.xaml
  9. 2
      samples/ControlCatalog/MainWindow.xaml
  10. 1
      samples/ControlCatalog/Pages/DialogsPage.xaml
  11. 14
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  12. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  13. 2
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  14. 17
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  15. 27
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  16. 28
      samples/RenderDemo/Pages/AnimationsPage.xaml
  17. 2
      samples/VirtualizationDemo/MainWindow.xaml
  18. 14
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  19. 2
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  20. 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  21. 2
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  22. 2
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  23. 2
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  24. 25
      src/Avalonia.Controls/Border.cs
  25. 2
      src/Avalonia.Controls/Button.cs
  26. 3
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  27. 2
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  28. 2
      src/Avalonia.Controls/Calendar/DatePicker.cs
  29. 4
      src/Avalonia.Controls/ComboBox.cs
  30. 20
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  31. 249
      src/Avalonia.Controls/ISelectionModel.cs
  32. 3
      src/Avalonia.Controls/Image.cs
  33. 180
      src/Avalonia.Controls/IndexPath.cs
  34. 232
      src/Avalonia.Controls/IndexRange.cs
  35. 19
      src/Avalonia.Controls/ListBox.cs
  36. 6
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  37. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  38. 2
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  39. 4
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  40. 15
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  41. 15
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  42. 4
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  43. 2
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  44. 46
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  45. 839
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  46. 4
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  47. 2
      src/Avalonia.Controls/RepeatButton.cs
  48. 2
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  49. 49
      src/Avalonia.Controls/SelectedItems.cs
  50. 848
      src/Avalonia.Controls/SelectionModel.cs
  51. 170
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  52. 83
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  53. 47
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  54. 966
      src/Avalonia.Controls/SelectionNode.cs
  55. 110
      src/Avalonia.Controls/SelectionNodeOperation.cs
  56. 2
      src/Avalonia.Controls/TabControl.cs
  57. 663
      src/Avalonia.Controls/TreeView.cs
  58. 46
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  59. 227
      src/Avalonia.Controls/Utils/SelectedItemsSync.cs
  60. 189
      src/Avalonia.Controls/Utils/SelectionTreeHelper.cs
  61. 2
      src/Avalonia.Controls/Window.cs
  62. 5
      src/Avalonia.Controls/WindowState.cs
  63. 3
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  64. 4
      src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs
  65. 90
      src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs
  66. 266
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  67. 472
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs
  68. 2
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/.gitignore
  69. 8878
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  70. 41
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json
  71. 57
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/FramePresenter.tsx
  72. 78
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/PreviewerServerConnection.ts
  73. 14
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html
  74. 15
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.tsx
  75. 35
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json
  76. 117
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js
  77. 53
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  78. 4
      src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
  79. 14
      src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs
  80. 20
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  81. 7
      src/Avalonia.Dialogs/ManagedFileDialogOptions.cs
  82. 4
      src/Avalonia.Input/DragEventArgs.cs
  83. 3
      src/Avalonia.Input/FocusManager.cs
  84. 4
      src/Avalonia.Input/Gestures.cs
  85. 6
      src/Avalonia.Input/PointerEventArgs.cs
  86. 4
      src/Avalonia.Input/Raw/RawDragEvent.cs
  87. 6
      src/Avalonia.Native/SystemDialogs.cs
  88. 2
      src/Avalonia.Native/WindowImpl.cs
  89. 4
      src/Avalonia.Remote.Protocol/BsonStreamTransport.cs
  90. 1
      src/Avalonia.Remote.Protocol/ITransport.cs
  91. 2
      src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs
  92. 9
      src/Avalonia.Remote.Protocol/TransportMessages.cs
  93. 2
      src/Avalonia.Remote.Protocol/ViewportMessages.cs
  94. 148
      src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
  95. 23
      src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs
  96. 40
      src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs
  97. 133
      src/Avalonia.Visuals/Media/BoxShadow.cs
  98. 137
      src/Avalonia.Visuals/Media/BoxShadows.cs
  99. 44
      src/Avalonia.Visuals/Media/Color.cs
  100. 6
      src/Avalonia.Visuals/Media/DrawingContext.cs

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
<PackageReference Include="SkiaSharp" Version="1.68.2.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2.1" />
</ItemGroup>
</Project>

3
native/Avalonia.Native/inc/avalonia-native.h

@ -135,6 +135,7 @@ enum AvnWindowState
Normal,
Minimized,
Maximized,
FullScreen,
};
enum AvnStandardCursorType
@ -246,7 +247,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0;
virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
virtual HRESULT SetDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0;
virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0;

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

@ -35,6 +35,10 @@ struct INSWindowHolder
struct IWindowStateChanged
{
virtual void WindowStateChanged () = 0;
virtual void StartStateTransition () = 0;
virtual void EndStateTransition () = 0;
virtual SystemDecorations Decorations () = 0;
virtual AvnWindowState WindowState () = 0;
};
#endif /* window_h */

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

@ -391,7 +391,7 @@ protected:
void UpdateStyle()
{
[Window setStyleMask:GetStyle()];
[Window setStyleMask: GetStyle()];
}
public:
@ -404,10 +404,13 @@ public:
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{
private:
bool _canResize = true;
SystemDecorations _hasDecorations = SystemDecorationsFull;
CGRect _lastUndecoratedFrame;
bool _canResize;
bool _fullScreenActive;
SystemDecorations _decorations;
AvnWindowState _lastWindowState;
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
@ -421,6 +424,11 @@ private:
ComPtr<IAvnWindowEvents> WindowEvents;
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
{
_fullScreenActive = false;
_canResize = true;
_decorations = SystemDecorationsFull;
_transitioningWindowState = false;
_inSetWindowState = false;
_lastWindowState = Normal;
WindowEvents = events;
[Window setCanBecomeKeyAndMain];
@ -428,6 +436,20 @@ private:
[Window setTabbingMode:NSWindowTabbingModeDisallowed];
}
void HideOrShowTrafficLights ()
{
for (id subview in Window.contentView.superview.subviews) {
if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
if ([button isKindOfClass:[NSButton class]]) {
[button setHidden: (_decorations != SystemDecorationsFull)];
}
}
}
}
}
virtual HRESULT Show () override
{
@autoreleasepool
@ -439,6 +461,8 @@ private:
WindowBaseImpl::Show();
HideOrShowTrafficLights();
return SetWindowState(_lastWindowState);
}
}
@ -459,41 +483,69 @@ private:
[cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show();
HideOrShowTrafficLights();
return S_OK;
}
}
void StartStateTransition () override
{
_transitioningWindowState = true;
}
void EndStateTransition () override
{
_transitioningWindowState = false;
}
SystemDecorations Decorations () override
{
return _decorations;
}
AvnWindowState WindowState () override
{
return _lastWindowState;
}
void WindowStateChanged () override
{
AvnWindowState state;
GetWindowState(&state);
WindowEvents->WindowStateChanged(state);
if(!_inSetWindowState && !_transitioningWindowState)
{
AvnWindowState state;
GetWindowState(&state);
if(_lastWindowState != state)
{
_lastWindowState = state;
WindowEvents->WindowStateChanged(state);
}
}
}
bool UndecoratedIsMaximized ()
{
return CGRectEqualToRect([Window frame], [Window screen].visibleFrame);
auto windowSize = [Window frame];
auto available = [Window screen].visibleFrame;
return CGRectEqualToRect(windowSize, available);
}
bool IsZoomed ()
{
return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized();
}
void DoZoom()
{
switch (_hasDecorations)
switch (_decorations)
{
case SystemDecorationsNone:
if (!UndecoratedIsMaximized())
{
_lastUndecoratedFrame = [Window frame];
}
[Window zoom:Window];
case SystemDecorationsBorderOnly:
[Window setFrame:[Window screen].visibleFrame display:true];
break;
case SystemDecorationsBorderOnly:
case SystemDecorationsFull:
[Window performZoom:Window];
break;
@ -510,25 +562,52 @@ private:
}
}
virtual HRESULT SetHasDecorations(SystemDecorations value) override
virtual HRESULT SetDecorations(SystemDecorations value) override
{
@autoreleasepool
{
_hasDecorations = value;
auto currentWindowState = _lastWindowState;
_decorations = value;
if(_fullScreenActive)
{
return S_OK;
}
auto currentFrame = [Window frame];
UpdateStyle();
HideOrShowTrafficLights();
switch (_hasDecorations)
switch (_decorations)
{
case SystemDecorationsNone:
[Window setHasShadow:NO];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
if(currentWindowState == Maximized)
{
if(!UndecoratedIsMaximized())
{
DoZoom();
}
}
break;
case SystemDecorationsBorderOnly:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
if(currentWindowState == Maximized)
{
if(!UndecoratedIsMaximized())
{
DoZoom();
}
}
break;
case SystemDecorationsFull:
@ -536,6 +615,13 @@ private:
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
if(currentWindowState == Maximized)
{
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
break;
}
@ -592,13 +678,19 @@ private:
return E_POINTER;
}
if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen)
{
*ret = FullScreen;
return S_OK;
}
if([Window isMiniaturized])
{
*ret = Minimized;
return S_OK;
}
if([Window isZoomed])
if(IsZoomed())
{
*ret = Maximized;
return S_OK;
@ -610,16 +702,57 @@ private:
}
}
void EnterFullScreenMode ()
{
_fullScreenActive = true;
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
[Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable];
[Window toggleFullScreen:nullptr];
}
void ExitFullScreenMode ()
{
[Window toggleFullScreen:nullptr];
_fullScreenActive = false;
SetDecorations(_decorations);
}
virtual HRESULT SetWindowState (AvnWindowState state) override
{
@autoreleasepool
{
if(_lastWindowState == state)
{
return S_OK;
}
_inSetWindowState = true;
auto currentState = _lastWindowState;
_lastWindowState = state;
if(currentState == Normal)
{
_preZoomSize = [Window frame];
}
if(_shown)
{
switch (state) {
case Maximized:
if(currentState == FullScreen)
{
ExitFullScreenMode();
}
lastPositionSet.X = 0;
lastPositionSet.Y = 0;
@ -635,40 +768,66 @@ private:
break;
case Minimized:
[Window miniaturize:Window];
if(currentState == FullScreen)
{
ExitFullScreenMode();
}
else
{
[Window miniaturize:Window];
}
break;
case FullScreen:
if([Window isMiniaturized])
{
[Window deminiaturize:Window];
}
EnterFullScreenMode();
break;
default:
case Normal:
if([Window isMiniaturized])
{
[Window deminiaturize:Window];
}
if(currentState == FullScreen)
{
ExitFullScreenMode();
}
if(IsZoomed())
{
DoZoom();
if(_decorations == SystemDecorationsFull)
{
DoZoom();
}
else
{
[Window setFrame:_preZoomSize display:true];
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
}
break;
}
}
_inSetWindowState = false;
return S_OK;
}
}
virtual void OnResized () override
{
if(_shown)
if(_shown && !_inSetWindowState && !_transitioningWindowState)
{
auto windowState = [Window isMiniaturized] ? Minimized
: (IsZoomed() ? Maximized : Normal);
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowEvents->WindowStateChanged(windowState);
}
WindowStateChanged();
}
}
@ -677,22 +836,23 @@ protected:
{
unsigned long s = NSWindowStyleMaskBorderless;
switch (_hasDecorations)
switch (_decorations)
{
case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
break;
case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
if(_canResize)
{
s = s | NSWindowStyleMaskResizable;
}
break;
}
@ -1171,6 +1331,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
}
}
- (void)performClose:(id)sender
{
if([[self delegate] respondsToSelector:@selector(windowShouldClose:)])
{
if(![[self delegate] windowShouldClose:self]) return;
}
else if([self respondsToSelector:@selector(windowShouldClose:)])
{
if(![self windowShouldClose:self]) return;
}
[self close];
}
- (void)pollModalSession:(nonnull NSModalSession)session
{
auto response = [NSApp runModalSession:session];
@ -1399,7 +1573,66 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)windowDidResize:(NSNotification *)notification
{
_parent->OnResized();
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->WindowStateChanged();
}
}
- (void)windowWillExitFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->StartStateTransition();
}
}
- (void)windowDidExitFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->EndStateTransition();
if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized)
{
NSRect screenRect = [[self screen] visibleFrame];
[self setFrame:screenRect display:YES];
}
if(parent->WindowState() == Minimized)
{
[self miniaturize:nullptr];
}
parent->WindowStateChanged();
}
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->StartStateTransition();
}
}
- (void)windowDidEnterFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->EndStateTransition();
parent->WindowStateChanged();
}
}
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame

14
nukebuild/Build.cs

@ -12,6 +12,7 @@ using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo;
@ -121,8 +122,21 @@ partial class Build : NukeBuild
EnsureCleanDirectory(Parameters.TestResultsRoot);
});
Target CompileHtmlPreviewer => _ => _
.DependsOn(Clean)
.Executes(() =>
{
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir));
NpmTasks.NpmRun(c => c
.SetWorkingDirectory(webappDir)
.SetCommand("dist"));
});
Target Compile => _ => _
.DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer)
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)

4
samples/BindingDemo/MainWindow.xaml

@ -74,11 +74,11 @@
</StackPanel.DataTemplates>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Multiple"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" SelectedItems="{Binding SelectedItems}"/>
<ListBox Items="{Binding Items}" SelectionMode="Multiple" Selection="{Binding Selection}"/>
</StackPanel>
<ContentControl Content="{Binding SelectedItems[0]}">
<ContentControl.DataTemplates>

5
samples/BindingDemo/ViewModels/MainWindowViewModel.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Threading;
using ReactiveUI;
using Avalonia.Controls;
namespace BindingDemo.ViewModels
{
@ -27,7 +28,7 @@ namespace BindingDemo.ViewModels
Detail = "Item " + x + " details",
}));
SelectedItems = new ObservableCollection<TestItem>();
Selection = new SelectionModel();
ShuffleItems = ReactiveCommand.Create(() =>
{
@ -56,7 +57,7 @@ namespace BindingDemo.ViewModels
}
public ObservableCollection<TestItem> Items { get; }
public ObservableCollection<TestItem> SelectedItems { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> ShuffleItems { get; }
public string BooleanString

5
samples/ControlCatalog/MainView.xaml

@ -60,8 +60,8 @@
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabControl.Tag>
<StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8">
<StackPanel Width="115" Spacing="4" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="8">
<ComboBox x:Name="Decorations" SelectedIndex="0">
<ComboBoxItem>No Decorations</ComboBoxItem>
<ComboBoxItem>Border Only</ComboBoxItem>
<ComboBoxItem>Full Decorations</ComboBoxItem>
@ -70,6 +70,7 @@
<ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem>
</ComboBox>
<ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
</StackPanel>
</TabControl.Tag>
</TabControl>

2
samples/ControlCatalog/MainWindow.xaml

@ -7,7 +7,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views"
x:Class="ControlCatalog.MainWindow">
x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
<NativeMenu.Menu>
<NativeMenu>

1
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -6,6 +6,7 @@
<Button Name="OpenFile">Open File</Button>
<Button Name="SaveFile">Save File</Button>
<Button Name="SelectFolder">Select Folder</Button>
<Button Name="OpenBoth">Select Both</Button>
<Button Name="DecoratedWindow">Decorated window</Button>
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button>

14
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Dialogs;
using Avalonia.Markup.Xaml;
#pragma warning disable 4014
@ -58,6 +59,19 @@ namespace ControlCatalog.Pages
Title = "Select folder",
}.ShowAsync(GetWindow());
};
this.FindControl<Button>("OpenBoth").Click += async delegate
{
var res = await new OpenFileDialog()
{
Title = "Select both",
AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
AllowDirectorySelection = true
});
if (res != null)
Console.WriteLine("Selected: \n" + string.Join("\n", res));
};
this.FindControl<Button>("DecoratedWindow").Click += delegate
{
new DecoratedWindow().Show();

2
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<Button Command="{Binding AddItemCommand}">Add</Button>

2
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -10,7 +10,7 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<TreeView Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView Items="{Binding Items}" Selection="{Binding Selection}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}"/>

17
samples/ControlCatalog/Pages/TreeViewPage.xaml.cs

@ -28,21 +28,22 @@ namespace ControlCatalog.Pages
{
Node root = new Node();
Items = root.Children;
SelectedItems = new ObservableCollection<Node>();
Selection = new SelectionModel();
AddItemCommand = ReactiveCommand.Create(() =>
{
Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
Node parentItem = Selection.SelectedItems.Count > 0 ?
(Node)Selection.SelectedItems[0] : root;
parentItem.AddNewItem();
});
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (SelectedItems.Count > 0)
while (Selection.SelectedItems.Count > 0)
{
Node lastItem = SelectedItems[0];
Node lastItem = (Node)Selection.SelectedItems[0];
RecursiveRemove(Items, lastItem);
SelectedItems.Remove(lastItem);
Selection.DeselectAt(Selection.SelectedIndices[0]);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
@ -67,7 +68,7 @@ namespace ControlCatalog.Pages
public ObservableCollection<Node> Items { get; }
public ObservableCollection<Node> SelectedItems { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
@ -78,7 +79,7 @@ namespace ControlCatalog.Pages
get => _selectionMode;
set
{
SelectedItems.Clear();
Selection.ClearSelection();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
@ -109,7 +110,7 @@ namespace ControlCatalog.Pages
public override string ToString() => Header;
private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" };
}
}
}

27
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,4 +1,5 @@
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Dialogs;
@ -11,6 +12,8 @@ namespace ControlCatalog.ViewModels
private IManagedNotificationManager _notificationManager;
private bool _isMenuItemChecked = true;
private WindowState _windowState;
private WindowState[] _windowStates;
public MainWindowViewModel(IManagedNotificationManager notificationManager)
{
@ -45,10 +48,32 @@ namespace ControlCatalog.ViewModels
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
});
ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
{
IsMenuItemChecked = !IsMenuItemChecked;
});
WindowState = WindowState.Normal;
WindowStates = new WindowState[]
{
WindowState.Minimized,
WindowState.Normal,
WindowState.Maximized,
WindowState.FullScreen,
};
}
public WindowState WindowState
{
get { return _windowState; }
set { this.RaiseAndSetIfChanged(ref _windowState, value); }
}
public WindowState[] WindowStates
{
get { return _windowStates; }
set { this.RaiseAndSetIfChanged(ref _windowStates, value); }
}
public IManagedNotificationManager NotificationManager

28
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -134,6 +134,32 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Shadow">
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red, -15 -15 Green"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue, -15 20 0 0 Blue"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="BoxShadow" Value="inset 0 0 20 30 Green, 20 20 20 0 Red"/>
</KeyFrame>
<KeyFrame Cue="85%">
<Setter Property="BoxShadow" Value="inset 30 0 20 30 Green, 20 20 20 10 Red"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -152,6 +178,8 @@
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Red"/>
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

2
samples/VirtualizationDemo/MainWindow.xaml

@ -45,7 +45,7 @@
<ListBox Name="listBox"
Items="{Binding Items}"
SelectedItems="{Binding SelectedItems}"
Selection="{Binding Selection}"
SelectionMode="Multiple"
VirtualizationMode="{Binding VirtualizationMode}"
ScrollViewer.HorizontalScrollBarVisibility="{Binding HorizontalScrollBarVisibility, Mode=TwoWay}"

14
samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs

@ -48,8 +48,7 @@ namespace VirtualizationDemo.ViewModels
set { this.RaiseAndSetIfChanged(ref _itemCount, value); }
}
public AvaloniaList<ItemViewModel> SelectedItems { get; }
= new AvaloniaList<ItemViewModel>();
public SelectionModel Selection { get; } = new SelectionModel();
public AvaloniaList<ItemViewModel> Items
{
@ -138,9 +137,9 @@ namespace VirtualizationDemo.ViewModels
{
var index = Items.Count;
if (SelectedItems.Count > 0)
if (Selection.SelectedIndices.Count > 0)
{
index = Items.IndexOf(SelectedItems[0]);
index = Selection.SelectedIndex.GetAt(0);
}
Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString));
@ -148,9 +147,9 @@ namespace VirtualizationDemo.ViewModels
private void Remove()
{
if (SelectedItems.Count > 0)
if (Selection.SelectedItems.Count > 0)
{
Items.RemoveAll(SelectedItems);
Items.RemoveAll(Selection.SelectedItems.Cast<ItemViewModel>().ToList());
}
}
@ -164,8 +163,7 @@ namespace VirtualizationDemo.ViewModels
private void SelectItem(int index)
{
SelectedItems.Clear();
SelectedItems.Add(Items[index]);
Selection.SelectedIndex = new IndexPath(index);
}
}
}

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

@ -164,7 +164,7 @@ namespace Avalonia.Controls
if (OwningGrid != null)
{
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.MouseButton == MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)

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

@ -457,7 +457,7 @@ namespace Avalonia.Controls
private void DataGridColumnHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (OwningColumn == null || e.Handled || !IsEnabled || e.MouseButton != MouseButton.Left)
if (OwningColumn == null || e.Handled || !IsEnabled || e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

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

@ -786,7 +786,7 @@ namespace Avalonia.Controls
private void DataGridRow_PointerPressed(PointerPressedEventArgs e)
{
if(e.MouseButton != MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

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

@ -277,7 +277,7 @@ namespace Avalonia.Controls
//TODO TabStop
private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e)
{
if (OwningGrid != null && e.MouseButton == MouseButton.Left)
if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled)
{

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

@ -164,7 +164,7 @@ namespace Avalonia.Controls.Primitives
//TODO TabStop
private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if(e.MouseButton != MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

25
src/Avalonia.Controls/Border.cs

@ -33,6 +33,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary>
@ -44,7 +50,8 @@ namespace Avalonia.Controls
BackgroundProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty);
CornerRadiusProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -83,14 +90,24 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Renders the control.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context)
{
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
}
/// <summary>
@ -110,8 +127,6 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
}
}

2
src/Avalonia.Controls/Button.cs

@ -278,7 +278,7 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
IsPressed = true;
e.Handled = true;

3
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -149,7 +149,8 @@ namespace Avalonia.Controls.Primitives
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
CalendarLeftMouseButtonDown?.Invoke(this, e);
}

2
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -206,7 +206,7 @@ namespace Avalonia.Controls.Primitives
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
CalendarDayButtonMouseDown?.Invoke(this, e);
}

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

@ -839,7 +839,7 @@ namespace Avalonia.Controls
}
private void Calendar_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (e.MouseButton == MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
e.Handled = true;
}

4
src/Avalonia.Controls/ComboBox.cs

@ -306,9 +306,9 @@ namespace Avalonia.Controls
{
var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
if (container == null && SelectedItems.Count > 0)
if (container == null && SelectedIndex != -1)
{
ScrollIntoView(SelectedItems[0]);
ScrollIntoView(Selection.SelectedIndex);
container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
}

20
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@ -94,9 +94,13 @@ namespace Avalonia.Controls.Generators
/// <returns>The container, or null of not found.</returns>
public IControl ContainerFromItem(object item)
{
IControl result;
_itemToContainer.TryGetValue(item, out result);
return result;
if (item != null)
{
_itemToContainer.TryGetValue(item, out var result);
return result;
}
return null;
}
/// <summary>
@ -106,9 +110,13 @@ namespace Avalonia.Controls.Generators
/// <returns>The item, or null of not found.</returns>
public object ItemFromContainer(IControl container)
{
object result;
_containerToItem.TryGetValue(container, out result);
return result;
if (container != null)
{
_containerToItem.TryGetValue(container, out var result);
return result;
}
return null;
}
}
}

249
src/Avalonia.Controls/ISelectionModel.cs

@ -0,0 +1,249 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the selected items for a control.
/// </summary>
public interface ISelectionModel : INotifyPropertyChanged
{
/// <summary>
/// Gets or sets the anchor index.
/// </summary>
IndexPath AnchorIndex { get; set; }
/// <summary>
/// Gets or set the index of the first selected item.
/// </summary>
IndexPath SelectedIndex { get; set; }
/// <summary>
/// Gets or set the indexes of the selected items.
/// </summary>
IReadOnlyList<IndexPath> SelectedIndices { get; }
/// <summary>
/// Gets the first selected item.
/// </summary>
object SelectedItem { get; }
/// <summary>
/// Gets the selected items.
/// </summary>
IReadOnlyList<object> SelectedItems { get; }
/// <summary>
/// Gets a value indicating whether the model represents a single or multiple selection.
/// </summary>
bool SingleSelect { get; set; }
/// <summary>
/// Gets a value indicating whether to always keep an item selected where possible.
/// </summary>
bool AutoSelect { get; set; }
/// <summary>
/// Gets or sets the collection that contains the items that can be selected.
/// </summary>
object Source { get; set; }
/// <summary>
/// Raised when the children of a selection are required.
/// </summary>
event EventHandler<SelectionModelChildrenRequestedEventArgs> ChildrenRequested;
/// <summary>
/// Raised when the selection has changed.
/// </summary>
event EventHandler<SelectionModelSelectionChangedEventArgs> SelectionChanged;
/// <summary>
/// Clears the selection.
/// </summary>
void ClearSelection();
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="index">The index of the item.</param>
void Deselect(int index);
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
void Deselect(int groupIndex, int itemIndex);
/// <summary>
/// Deselects an item.
/// </summary>
/// <param name="index">The index of the item.</param>
void DeselectAt(IndexPath index);
/// <summary>
/// Deselects a range of items.
/// </summary>
/// <param name="start">The start index of the range.</param>
/// <param name="end">The end index of the range.</param>
void DeselectRange(IndexPath start, IndexPath end);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void DeselectRangeFromAnchor(int index);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="endGroupIndex">
/// The index of the item group that represents the end of the selection.
/// </param>
/// <param name="endItemIndex">
/// The index of the item in the group that represents the end of the selection.
/// </param>
void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex);
/// <summary>
/// Deselects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void DeselectRangeFromAnchorTo(IndexPath index);
/// <summary>
/// Disposes the object and clears the selection.
/// </summary>
void Dispose();
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="index">The index of the item</param>
bool IsSelected(int index);
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
bool IsSelected(int groupIndex, int itemIndex);
/// <summary>
/// Checks whether an item is selected.
/// </summary>
/// <param name="index">The index of the item</param>
public bool IsSelectedAt(IndexPath index);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="index">The index of the item</param>
/// <returns>
/// True if the item and all its descendents are selected, false if the item and all its
/// descendents are deselected, or null if a combination of selected and deselected.
/// </returns>
bool? IsSelectedWithPartial(int index);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
/// <returns>
/// True if the item and all its descendents are selected, false if the item and all its
/// descendents are deselected, or null if a combination of selected and deselected.
/// </returns>
bool? IsSelectedWithPartial(int groupIndex, int itemIndex);
/// <summary>
/// Checks whether an item or its descendents are selected.
/// </summary>
/// <param name="index">The index of the item</param>
/// <returns>
/// True if the item and all its descendents are selected, false if the item and all its
/// descendents are deselected, or null if a combination of selected and deselected.
/// </returns>
bool? IsSelectedWithPartialAt(IndexPath index);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="index">The index of the item</param>
void Select(int index);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="itemIndex">The index of the item in the group.</param>
void Select(int groupIndex, int itemIndex);
/// <summary>
/// Selects an item.
/// </summary>
/// <param name="index">The index of the item</param>
void SelectAt(IndexPath index);
/// <summary>
/// Selects all items.
/// </summary>
void SelectAll();
/// <summary>
/// Selects a range of items.
/// </summary>
/// <param name="start">The start index of the range.</param>
/// <param name="end">The end index of the range.</param>
void SelectRange(IndexPath start, IndexPath end);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void SelectRangeFromAnchor(int index);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="endGroupIndex">
/// The index of the item group that represents the end of the selection.
/// </param>
/// <param name="endItemIndex">
/// The index of the item in the group that represents the end of the selection.
/// </param>
void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex);
/// <summary>
/// Selects a range of items, starting at <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The end index of the range.</param>
void SelectRangeFromAnchorTo(IndexPath index);
/// <summary>
/// Sets the <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="index">The anchor index.</param>
void SetAnchorIndex(int index);
/// <summary>
/// Sets the <see cref="AnchorIndex"/>.
/// </summary>
/// <param name="groupIndex">The index of the item group.</param>
/// <param name="index">The index of the item in the group.</param>
void SetAnchorIndex(int groupIndex, int index);
/// <summary>
/// Begins a batch update of the selection.
/// </summary>
/// <returns>An <see cref="IDisposable"/> that finishes the batch update.</returns>
IDisposable Update();
}
}

3
src/Avalonia.Controls/Image.cs

@ -69,10 +69,11 @@ namespace Avalonia.Controls
{
var source = Source;
if (source != null)
if (source != null && Bounds.Width > 0 && Bounds.Height > 0)
{
Rect viewPort = new Rect(Bounds.Size);
Size sourceSize = source.Size;
Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
Size scaledSize = sourceSize * scale;
Rect destRect = viewPort

180
src/Avalonia.Controls/IndexPath.cs

@ -0,0 +1,180 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls
{
public readonly struct IndexPath : IComparable<IndexPath>, IEquatable<IndexPath>
{
public static readonly IndexPath Unselected = default;
private readonly int _index;
private readonly int[]? _path;
public IndexPath(int index)
{
_index = index + 1;
_path = null;
}
public IndexPath(int groupIndex, int itemIndex)
{
_index = 0;
_path = new[] { groupIndex, itemIndex };
}
public IndexPath(IEnumerable<int>? indices)
{
if (indices != null)
{
_index = 0;
_path = indices.ToArray();
}
else
{
_index = 0;
_path = null;
}
}
private IndexPath(int[] basePath, int index)
{
basePath = basePath ?? throw new ArgumentNullException(nameof(basePath));
_index = 0;
_path = new int[basePath.Length + 1];
Array.Copy(basePath, _path, basePath.Length);
_path[basePath.Length] = index;
}
public int GetSize() => _path?.Length ?? (_index == 0 ? 0 : 1);
public int GetAt(int index)
{
if (index >= GetSize())
{
throw new IndexOutOfRangeException();
}
return _path?[index] ?? (_index - 1);
}
public int CompareTo(IndexPath other)
{
var rhsPath = other;
int compareResult = 0;
int lhsCount = GetSize();
int rhsCount = rhsPath.GetSize();
if (lhsCount == 0 || rhsCount == 0)
{
// one of the paths are empty, compare based on size
compareResult = (lhsCount - rhsCount);
}
else
{
// both paths are non-empty, but can be of different size
for (int i = 0; i < Math.Min(lhsCount, rhsCount); i++)
{
if (GetAt(i) < rhsPath.GetAt(i))
{
compareResult = -1;
break;
}
else if (GetAt(i) > rhsPath.GetAt(i))
{
compareResult = 1;
break;
}
}
// if both match upto min(lhsCount, rhsCount), compare based on size
compareResult = compareResult == 0 ? (lhsCount - rhsCount) : compareResult;
}
if (compareResult != 0)
{
compareResult = compareResult > 0 ? 1 : -1;
}
return compareResult;
}
public IndexPath CloneWithChildIndex(int childIndex)
{
if (_path != null)
{
return new IndexPath(_path, childIndex);
}
else if (_index != 0)
{
return new IndexPath(_index - 1, childIndex);
}
else
{
return new IndexPath(childIndex);
}
}
public override string ToString()
{
if (_path != null)
{
return "R" + string.Join(".", _path);
}
else if (_index != 0)
{
return "R" + (_index - 1);
}
else
{
return "R";
}
}
public static IndexPath CreateFrom(int index) => new IndexPath(index);
public static IndexPath CreateFrom(int groupIndex, int itemIndex) => new IndexPath(groupIndex, itemIndex);
public static IndexPath CreateFromIndices(IList<int> indices) => new IndexPath(indices);
public override bool Equals(object obj) => obj is IndexPath other && Equals(other);
public bool Equals(IndexPath other) => CompareTo(other) == 0;
public override int GetHashCode()
{
var hashCode = -504981047;
if (_path != null)
{
foreach (var i in _path)
{
hashCode = hashCode * -1521134295 + i.GetHashCode();
}
}
else
{
hashCode = hashCode * -1521134295 + _index.GetHashCode();
}
return hashCode;
}
public static bool operator <(IndexPath x, IndexPath y) { return x.CompareTo(y) < 0; }
public static bool operator >(IndexPath x, IndexPath y) { return x.CompareTo(y) > 0; }
public static bool operator <=(IndexPath x, IndexPath y) { return x.CompareTo(y) <= 0; }
public static bool operator >=(IndexPath x, IndexPath y) { return x.CompareTo(y) >= 0; }
public static bool operator ==(IndexPath x, IndexPath y) { return x.CompareTo(y) == 0; }
public static bool operator !=(IndexPath x, IndexPath y) { return x.CompareTo(y) != 0; }
public static bool operator ==(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) == 0; }
public static bool operator !=(IndexPath? x, IndexPath? y) { return (x ?? default).CompareTo(y ?? default) != 0; }
}
}

232
src/Avalonia.Controls/IndexRange.cs

@ -0,0 +1,232 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
internal readonly struct IndexRange : IEquatable<IndexRange>
{
private static readonly IndexRange s_invalid = new IndexRange(int.MinValue, int.MinValue);
public IndexRange(int begin, int end)
{
// Accept out of order begin/end pairs, just swap them.
if (begin > end)
{
int temp = begin;
begin = end;
end = temp;
}
Begin = begin;
End = end;
}
public int Begin { get; }
public int End { get; }
public int Count => (End - Begin) + 1;
public bool Contains(int index) => index >= Begin && index <= End;
public bool Split(int splitIndex, out IndexRange before, out IndexRange after)
{
bool afterIsValid;
before = new IndexRange(Begin, splitIndex);
if (splitIndex < End)
{
after = new IndexRange(splitIndex + 1, End);
afterIsValid = true;
}
else
{
after = new IndexRange();
afterIsValid = false;
}
return afterIsValid;
}
public bool Intersects(IndexRange other)
{
return (Begin <= other.End) && (End >= other.Begin);
}
public bool Adjacent(IndexRange other)
{
return Begin == other.End + 1 || End == other.Begin - 1;
}
public override bool Equals(object? obj)
{
return obj is IndexRange range && Equals(range);
}
public bool Equals(IndexRange other)
{
return Begin == other.Begin && End == other.End;
}
public override int GetHashCode()
{
var hashCode = 1903003160;
hashCode = hashCode * -1521134295 + Begin.GetHashCode();
hashCode = hashCode * -1521134295 + End.GetHashCode();
return hashCode;
}
public override string ToString() => $"[{Begin}..{End}]";
public static bool operator ==(IndexRange left, IndexRange right) => left.Equals(right);
public static bool operator !=(IndexRange left, IndexRange right) => !(left == right);
public static int Add(
IList<IndexRange> ranges,
IndexRange range,
IList<IndexRange>? added = null)
{
var result = 0;
for (var i = 0; i < ranges.Count && range != s_invalid; ++i)
{
var existing = ranges[i];
if (range.Intersects(existing) || range.Adjacent(existing))
{
if (range.Begin < existing.Begin)
{
var add = new IndexRange(range.Begin, existing.Begin - 1);
ranges[i] = new IndexRange(range.Begin, existing.End);
added?.Add(add);
result += add.Count;
}
range = range.End <= existing.End ?
s_invalid :
new IndexRange(existing.End + 1, range.End);
}
else if (range.End < existing.Begin)
{
ranges.Insert(i, range);
added?.Add(range);
result += range.Count;
range = s_invalid;
}
}
if (range != s_invalid)
{
ranges.Add(range);
added?.Add(range);
result += range.Count;
}
MergeRanges(ranges);
return result;
}
public static int Remove(
IList<IndexRange> ranges,
IndexRange range,
IList<IndexRange>? removed = null)
{
var result = 0;
for (var i = 0; i < ranges.Count; ++i)
{
var existing = ranges[i];
if (range.Intersects(existing))
{
if (range.Begin <= existing.Begin && range.End >= existing.End)
{
ranges.RemoveAt(i--);
removed?.Add(existing);
result += existing.Count;
}
else if (range.Begin > existing.Begin && range.End >= existing.End)
{
ranges[i] = new IndexRange(existing.Begin, range.Begin - 1);
removed?.Add(new IndexRange(range.Begin, existing.End));
result += existing.End - (range.Begin - 1);
}
else if (range.Begin > existing.Begin && range.End < existing.End)
{
ranges[i] = new IndexRange(existing.Begin, range.Begin - 1);
ranges.Insert(++i, new IndexRange(range.End + 1, existing.End));
removed?.Add(range);
result += range.Count;
}
else if (range.End <= existing.End)
{
var remove = new IndexRange(existing.Begin, range.End);
ranges[i] = new IndexRange(range.End + 1, existing.End);
removed?.Add(remove);
result += remove.Count;
}
}
}
return result;
}
public static IEnumerable<IndexRange> Subtract(
IndexRange lhs,
IEnumerable<IndexRange> rhs)
{
var result = new List<IndexRange> { lhs };
foreach (var range in rhs)
{
Remove(result, range);
}
return result;
}
public static IEnumerable<int> EnumerateIndices(IEnumerable<IndexRange> ranges)
{
foreach (var range in ranges)
{
for (var i = range.Begin; i <= range.End; ++i)
{
yield return i;
}
}
}
public static int GetCount(IEnumerable<IndexRange> ranges)
{
var result = 0;
foreach (var range in ranges)
{
result += (range.End - range.Begin) + 1;
}
return result;
}
private static void MergeRanges(IList<IndexRange> ranges)
{
for (var i = ranges.Count - 2; i >= 0; --i)
{
var r = ranges[i];
var r1 = ranges[i + 1];
if (r.Intersects(r1) || r.End == r1.Begin - 1)
{
ranges[i] = new IndexRange(r.Begin, r1.End);
ranges.RemoveAt(i + 1);
}
}
}
}
}

19
src/Avalonia.Controls/ListBox.cs

@ -31,6 +31,12 @@ namespace Avalonia.Controls
public static readonly new DirectProperty<SelectingItemsControl, IList> SelectedItemsProperty =
SelectingItemsControl.SelectedItemsProperty;
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly new DirectProperty<SelectingItemsControl, ISelectionModel> SelectionProperty =
SelectingItemsControl.SelectionProperty;
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
@ -70,6 +76,15 @@ namespace Avalonia.Controls
set => base.SelectedItems = value;
}
/// <summary>
/// Gets or sets a model holding the current selection.
/// </summary>
public new ISelectionModel Selection
{
get => base.Selection;
set => base.Selection = value;
}
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
@ -95,12 +110,12 @@ namespace Avalonia.Controls
/// <summary>
/// Selects all items in the <see cref="ListBox"/>.
/// </summary>
public new void SelectAll() => base.SelectAll();
public void SelectAll() => Selection.SelectAll();
/// <summary>
/// Deselects all items in the <see cref="ListBox"/>.
/// </summary>
public new void UnselectAll() => base.UnselectAll();
public void UnselectAll() => Selection.ClearSelection();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()

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

@ -5,6 +5,7 @@ using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
#nullable enable
@ -338,8 +339,9 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerPressed(object sender, PointerPressedEventArgs e)
{
var item = GetMenuItem(e.Source as IControl);
var visual = (IVisual)sender;
if (e.MouseButton == MouseButton.Left && item?.HasSubMenu == true)
if (e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed && item?.HasSubMenu == true)
{
if (item.IsSubMenuOpen)
{
@ -392,7 +394,7 @@ namespace Avalonia.Controls.Platform
{
var control = e.Source as ILogical;
if (!Menu.IsLogicalParentOf(control))
if (!Menu.IsLogicalAncestorOf(control))
{
Menu.Close();
}

21
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -40,7 +40,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
Border.BoxShadowProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
@ -132,6 +137,15 @@ namespace Avalonia.Controls.Presenters
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Gets the control displayed by the presenter.
/// </summary>
@ -274,7 +288,8 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
public override void Render(DrawingContext context)
{
_borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
_borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
}
/// <summary>
@ -321,8 +336,6 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
return ArrangeOverrideImpl(finalSize, new Vector());
}

2
src/Avalonia.Controls/Presenters/IItemsPresenter.cs

@ -11,6 +11,6 @@ namespace Avalonia.Controls.Presenters
void ItemsChanged(NotifyCollectionChangedEventArgs e);
void ScrollIntoView(object item);
void ScrollIntoView(int index);
}
}

4
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@ -275,8 +275,8 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public virtual void ScrollIntoView(object item)
/// <param name="index">The index of the item.</param>
public virtual void ScrollIntoView(int index)
{
}

15
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -64,18 +64,13 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="item">The item.</param>
public override void ScrollIntoView(object item)
/// <param name="index">The index of the item.</param>
public override void ScrollIntoView(int index)
{
if (Items != null)
if (index != -1)
{
var index = Items.IndexOf(item);
if (index != -1)
{
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
container?.BringIntoView();
}
var container = Owner.ItemContainerGenerator.ContainerFromIndex(index);
container?.BringIntoView();
}
}

15
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -286,20 +286,15 @@ namespace Avalonia.Controls.Presenters
break;
}
return ScrollIntoView(newItemIndex);
return ScrollIntoViewCore(newItemIndex);
}
/// <inheritdoc/>
public override void ScrollIntoView(object item)
public override void ScrollIntoView(int index)
{
if (Items != null)
if (index != -1)
{
var index = Items.IndexOf(item);
if (index != -1)
{
ScrollIntoView(index);
}
ScrollIntoViewCore(index);
}
}
@ -511,7 +506,7 @@ namespace Avalonia.Controls.Presenters
/// </summary>
/// <param name="index">The item index.</param>
/// <returns>The container that was brought into view.</returns>
private IControl ScrollIntoView(int index)
private IControl ScrollIntoViewCore(int index)
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;

4
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -128,9 +128,9 @@ namespace Avalonia.Controls.Presenters
_scrollInvalidated?.Invoke(this, e);
}
public override void ScrollIntoView(object item)
public override void ScrollIntoView(int index)
{
Virtualizer?.ScrollIntoView(item);
Virtualizer?.ScrollIntoView(index);
}
/// <inheritdoc/>

2
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters
}
/// <inheritdoc/>
public virtual void ScrollIntoView(object item)
public virtual void ScrollIntoView(int index)
{
}

46
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -74,16 +74,15 @@ namespace Avalonia.Controls.Presenters
static TextPresenter()
{
AffectsRender<TextPresenter>(PasswordCharProperty,
SelectionBrushProperty, SelectionForegroundBrushProperty,
SelectionStartProperty, SelectionEndProperty);
AffectsRender<TextPresenter>(SelectionBrushProperty);
Observable.Merge(
TextProperty.Changed,
SelectionStartProperty.Changed,
SelectionEndProperty.Changed,
PasswordCharProperty.Changed
).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
SelectionStartProperty.Changed, SelectionEndProperty.Changed,
SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed
).AddClassHandler<TextPresenter>((x, _) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
}
@ -184,7 +183,7 @@ namespace Avalonia.Controls.Presenters
{
get
{
return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text));
return _formattedText ?? (_formattedText = CreateFormattedText());
}
}
@ -219,7 +218,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
public IBrush CaretBrush
{
get => GetValue(CaretBrushProperty);
@ -284,13 +283,9 @@ namespace Avalonia.Controls.Presenters
/// </summary>
protected void InvalidateFormattedText()
{
if (_formattedText != null)
{
_constraint = _formattedText.Constraint;
_formattedText = null;
}
_formattedText = null;
InvalidateVisual();
InvalidateMeasure();
}
/// <summary>
@ -307,6 +302,7 @@ namespace Avalonia.Controls.Presenters
}
FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText);
}
@ -424,20 +420,20 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns>
protected virtual FormattedText CreateFormattedText(Size constraint, string text)
protected virtual FormattedText CreateFormattedText()
{
FormattedText result = null;
var text = Text;
if (PasswordChar != default(char))
{
result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0));
result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
}
else
{
result = CreateFormattedTextInternal(constraint, text);
result = CreateFormattedTextInternal(_constraint, text);
}
var selectionStart = SelectionStart;
@ -467,13 +463,15 @@ namespace Avalonia.Controls.Presenters
{
if (TextWrapping == TextWrapping.Wrap)
{
FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
_constraint = new Size(availableSize.Width, double.PositiveInfinity);
}
else
{
FormattedText.Constraint = Size.Infinity;
_constraint = Size.Infinity;
}
_formattedText = null;
return FormattedText.Bounds.Size;
}

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

File diff suppressed because it is too large

4
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -115,7 +115,7 @@ namespace Avalonia.Controls.Remote.Server
{
lock (_lock)
{
_lastReceivedFrame = lastFrame.SequenceId;
_lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame);
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
@ -298,6 +298,8 @@ namespace Avalonia.Controls.Remote.Server
Width = width,
Height = height,
Stride = width * bpp,
DpiX = _dpi.X,
DpiY = _dpi.Y
};
}

2
src/Avalonia.Controls/RepeatButton.cs

@ -88,7 +88,7 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
StartTimer();
}

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

@ -96,6 +96,8 @@ namespace Avalonia.Controls
/// <returns>the item.</returns>
public object GetAt(int index) => _inner[index];
public int IndexOf(object item) => _inner.IndexOf(item);
/// <summary>
/// Retrieves the index of the item that has the specified unique identifier (key).
/// </summary>

49
src/Avalonia.Controls/SelectedItems.cs

@ -0,0 +1,49 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
public interface ISelectedItemInfo
{
public IndexPath Path { get; }
}
internal class SelectedItems<TValue, Tinfo> : IReadOnlyList<TValue>
where Tinfo : ISelectedItemInfo
{
private readonly List<Tinfo> _infos;
private readonly Func<List<Tinfo>, int, TValue> _getAtImpl;
public SelectedItems(
List<Tinfo> infos,
int count,
Func<List<Tinfo>, int, TValue> getAtImpl)
{
_infos = infos;
_getAtImpl = getAtImpl;
Count = count;
}
public TValue this[int index] => _getAtImpl(_infos, index);
public int Count { get; }
public IEnumerator<TValue> GetEnumerator()
{
for (var i = 0; i < Count; ++i)
{
yield return this[i];
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

848
src/Avalonia.Controls/SelectionModel.cs

@ -0,0 +1,848 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Utils;
#nullable enable
namespace Avalonia.Controls
{
public class SelectionModel : ISelectionModel, IDisposable
{
private readonly SelectionNode _rootNode;
private bool _singleSelect;
private bool _autoSelect;
private int _operationCount;
private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
public event EventHandler<SelectionModelChildrenRequestedEventArgs>? ChildrenRequested;
public event PropertyChangedEventHandler? PropertyChanged;
public event EventHandler<SelectionModelSelectionChangedEventArgs>? SelectionChanged;
public SelectionModel()
{
_rootNode = new SelectionNode(this, null);
SharedLeafNode = new SelectionNode(this, null);
}
public object? Source
{
get => _rootNode.Source;
set
{
if (_rootNode.Source != value)
{
var raiseChanged = _rootNode.Source == null && SelectedIndices.Count > 0;
if (_rootNode.Source != null)
{
if (_rootNode.Source != null)
{
using (var operation = new Operation(this))
{
ClearSelection(resetAnchor: true);
}
}
}
_rootNode.Source = value;
ApplyAutoSelect();
RaisePropertyChanged("Source");
if (raiseChanged)
{
var e = new SelectionModelSelectionChangedEventArgs(
null,
SelectedIndices,
null,
SelectedItems);
OnSelectionChanged(e);
}
}
}
}
public bool SingleSelect
{
get => _singleSelect;
set
{
if (_singleSelect != value)
{
_singleSelect = value;
var selectedIndices = SelectedIndices;
if (value && selectedIndices != null && selectedIndices.Count > 0)
{
using var operation = new Operation(this);
// We want to be single select, so make sure there is only
// one selected item.
var firstSelectionIndexPath = selectedIndices[0];
ClearSelection(resetAnchor: true);
SelectWithPathImpl(firstSelectionIndexPath, select: true);
SelectedIndex = firstSelectionIndexPath;
}
RaisePropertyChanged("SingleSelect");
}
}
}
public bool RetainSelectionOnReset
{
get => _rootNode.RetainSelectionOnReset;
set => _rootNode.RetainSelectionOnReset = value;
}
public bool AutoSelect
{
get => _autoSelect;
set
{
if (_autoSelect != value)
{
_autoSelect = value;
ApplyAutoSelect();
}
}
}
public IndexPath AnchorIndex
{
get
{
IndexPath anchor = default;
if (_rootNode.AnchorIndex >= 0)
{
var path = new List<int>();
SelectionNode? current = _rootNode;
while (current?.AnchorIndex >= 0)
{
path.Add(current.AnchorIndex);
current = current.GetAt(current.AnchorIndex, false);
}
anchor = new IndexPath(path);
}
return anchor;
}
set
{
if (value != null)
{
SelectionTreeHelper.TraverseIndexPath(
_rootNode,
value,
realizeChildren: true,
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth));
}
else
{
_rootNode.AnchorIndex = -1;
}
RaisePropertyChanged("AnchorIndex");
}
}
public IndexPath SelectedIndex
{
get
{
IndexPath selectedIndex = default;
var selectedIndices = SelectedIndices;
if (selectedIndices?.Count > 0)
{
selectedIndex = selectedIndices[0];
}
return selectedIndex;
}
set
{
var isSelected = IsSelectedWithPartialAt(value);
if (!IsSelectedAt(value) || SelectedItems.Count > 1)
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
SelectWithPathImpl(value, select: true);
ApplyAutoSelect();
}
}
}
public object? SelectedItem
{
get
{
object? item = null;
var selectedItems = SelectedItems;
if (selectedItems?.Count > 0)
{
item = selectedItems[0];
}
return item;
}
}
public IReadOnlyList<object?> SelectedItems
{
get
{
if (_selectedItemsCached == null)
{
var selectedInfos = new List<SelectedItemInfo>();
var count = 0;
if (_rootNode.Source != null)
{
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: false,
currentInfo =>
{
if (currentInfo.Node.SelectedCount > 0)
{
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
count += currentInfo.Node.SelectedCount;
}
});
}
// Instead of creating a dumb vector that takes up the space for all the selected items,
// we create a custom IReadOnlyList implementation that calls back using a delegate to find
// the selected item at a particular index. This avoid having to create the storage and copying
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
// easier to consume flat vector view of objects.
var selectedItems = new SelectedItems<object?, SelectedItemInfo> (
selectedInfos,
count,
(infos, index) =>
{
var currentIndex = 0;
object? item = null;
foreach (var info in infos)
{
var node = info.Node;
if (node != null)
{
var currentCount = node.SelectedCount;
if (index >= currentIndex && index < currentIndex + currentCount)
{
var targetIndex = node.SelectedIndices[index - currentIndex];
item = node.ItemsSourceView!.GetAt(targetIndex);
break;
}
currentIndex += currentCount;
}
else
{
throw new InvalidOperationException(
"Selection has changed since SelectedItems property was read.");
}
}
return item;
});
_selectedItemsCached = selectedItems;
}
return _selectedItemsCached;
}
}
public IReadOnlyList<IndexPath> SelectedIndices
{
get
{
if (_selectedIndicesCached == null)
{
var selectedInfos = new List<SelectedItemInfo>();
var count = 0;
SelectionTreeHelper.Traverse(
_rootNode,
false,
currentInfo =>
{
if (currentInfo.Node.SelectedCount > 0)
{
selectedInfos.Add(new SelectedItemInfo(currentInfo.Node, currentInfo.Path));
count += currentInfo.Node.SelectedCount;
}
});
// Instead of creating a dumb vector that takes up the space for all the selected indices,
// we create a custom VectorView implimentation that calls back using a delegate to find
// the IndexPath at a particular index. This avoid having to create the storage and copying
// needed in a dumb vector. This also allows us to expose a tree of selected nodes into an
// easier to consume flat vector view of IndexPaths.
var indices = new SelectedItems<IndexPath, SelectedItemInfo>(
selectedInfos,
count,
(infos, index) => // callback for GetAt(index)
{
var currentIndex = 0;
IndexPath path = default;
foreach (var info in infos)
{
var node = info.Node;
if (node != null)
{
var currentCount = node.SelectedCount;
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = node.SelectedIndices[index - currentIndex];
path = info.Path.CloneWithChildIndex(targetIndex);
break;
}
currentIndex += currentCount;
}
else
{
throw new InvalidOperationException(
"Selection has changed since SelectedIndices property was read.");
}
}
return path;
});
_selectedIndicesCached = indices;
}
return _selectedIndicesCached;
}
}
internal SelectionNode SharedLeafNode { get; private set; }
public void Dispose()
{
ClearSelection(resetAnchor: false);
_rootNode.Cleanup();
_rootNode.Dispose();
_selectedIndicesCached = null;
_selectedItemsCached = null;
}
public void SetAnchorIndex(int index) => AnchorIndex = new IndexPath(index);
public void SetAnchorIndex(int groupIndex, int index) => AnchorIndex = new IndexPath(groupIndex, index);
public void Select(int index)
{
using var operation = new Operation(this);
SelectImpl(index, select: true);
}
public void Select(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: true);
}
public void SelectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: true);
}
public void Deselect(int index)
{
using var operation = new Operation(this);
SelectImpl(index, select: false);
ApplyAutoSelect();
}
public void Deselect(int groupIndex, int itemIndex)
{
using var operation = new Operation(this);
SelectWithGroupImpl(groupIndex, itemIndex, select: false);
ApplyAutoSelect();
}
public void DeselectAt(IndexPath index)
{
using var operation = new Operation(this);
SelectWithPathImpl(index, select: false);
ApplyAutoSelect();
}
public bool IsSelected(int index) => _rootNode.IsSelected(index);
public bool IsSelected(int grouIndex, int itemIndex)
{
return IsSelectedAt(new IndexPath(grouIndex, itemIndex));
}
public bool IsSelectedAt(IndexPath index)
{
var path = index;
SelectionNode? node = _rootNode;
for (int i = 0; i < path.GetSize() - 1; i++)
{
var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, realizeChild: false);
if (node == null)
{
return false;
}
}
return node.IsSelected(index.GetAt(index.GetSize() - 1));
}
public bool? IsSelectedWithPartial(int index)
{
if (index < 0)
{
throw new ArgumentException("Index must be >= 0", nameof(index));
}
var isSelected = _rootNode.IsSelectedWithPartial(index);
return isSelected;
}
public bool? IsSelectedWithPartial(int groupIndex, int itemIndex)
{
if (groupIndex < 0)
{
throw new ArgumentException("Group index must be >= 0", nameof(groupIndex));
}
if (itemIndex < 0)
{
throw new ArgumentException("Item index must be >= 0", nameof(itemIndex));
}
var isSelected = (bool?)false;
var childNode = _rootNode.GetAt(groupIndex, realizeChild: false);
if (childNode != null)
{
isSelected = childNode.IsSelectedWithPartial(itemIndex);
}
return isSelected;
}
public bool? IsSelectedWithPartialAt(IndexPath index)
{
var path = index;
var isRealized = true;
SelectionNode? node = _rootNode;
for (int i = 0; i < path.GetSize() - 1; i++)
{
var childIndex = path.GetAt(i);
node = node.GetAt(childIndex, realizeChild: false);
if (node == null)
{
isRealized = false;
break;
}
}
var isSelected = (bool?)false;
if (isRealized)
{
var size = path.GetSize();
if (size == 0)
{
isSelected = SelectionNode.ConvertToNullableBool(node!.EvaluateIsSelectedBasedOnChildrenNodes());
}
else
{
isSelected = node!.IsSelectedWithPartial(path.GetAt(size - 1));
}
}
return isSelected;
}
public void SelectRangeFromAnchor(int index)
{
using var operation = new Operation(this);
SelectRangeFromAnchorImpl(index, select: true);
}
public void SelectRangeFromAnchor(int endGroupIndex, int endItemIndex)
{
using var operation = new Operation(this);
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, select: true);
}
public void SelectRangeFromAnchorTo(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: true);
}
public void DeselectRangeFromAnchor(int index)
{
using var operation = new Operation(this);
SelectRangeFromAnchorImpl(index, select: false);
}
public void DeselectRangeFromAnchor(int endGroupIndex, int endItemIndex)
{
using var operation = new Operation(this);
SelectRangeFromAnchorWithGroupImpl(endGroupIndex, endItemIndex, false /* select */);
}
public void DeselectRangeFromAnchorTo(IndexPath index)
{
using var operation = new Operation(this);
SelectRangeImpl(AnchorIndex, index, select: false);
}
public void SelectRange(IndexPath start, IndexPath end)
{
using var operation = new Operation(this);
SelectRangeImpl(start, end, select: true);
}
public void DeselectRange(IndexPath start, IndexPath end)
{
using var operation = new Operation(this);
SelectRangeImpl(start, end, select: false);
}
public void SelectAll()
{
using var operation = new Operation(this);
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: true,
info =>
{
if (info.Node.DataCount > 0)
{
info.Node.SelectAll();
}
});
}
public void ClearSelection()
{
using var operation = new Operation(this);
ClearSelection(resetAnchor: true);
ApplyAutoSelect();
}
public IDisposable Update() => new Operation(this);
protected void OnPropertyChanged(string propertyName)
{
RaisePropertyChanged(propertyName);
}
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnSelectionInvalidatedDueToCollectionChange(
bool selectionInvalidated,
IReadOnlyList<object?>? removedItems)
{
SelectionModelSelectionChangedEventArgs? e = null;
if (selectionInvalidated)
{
e = new SelectionModelSelectionChangedEventArgs(null, null, removedItems, null);
}
OnSelectionChanged(e);
ApplyAutoSelect();
}
internal IObservable<object?>? ResolvePath(object data, IndexPath dataIndexPath)
{
IObservable<object?>? resolved = null;
// Raise ChildrenRequested event if there is a handler
if (ChildrenRequested != null)
{
if (_childrenRequestedEventArgs == null)
{
_childrenRequestedEventArgs = new SelectionModelChildrenRequestedEventArgs(data, dataIndexPath, false);
}
else
{
_childrenRequestedEventArgs.Initialize(data, dataIndexPath, false);
}
ChildrenRequested(this, _childrenRequestedEventArgs);
resolved = _childrenRequestedEventArgs.Children;
// Clear out the values in the args so that it cannot be used after the event handler call.
_childrenRequestedEventArgs.Initialize(null, default, true);
}
return resolved;
}
private void ClearSelection(bool resetAnchor)
{
SelectionTreeHelper.Traverse(
_rootNode,
realizeChildren: false,
info => info.Node.Clear());
if (resetAnchor)
{
AnchorIndex = default;
}
}
private void OnSelectionChanged(SelectionModelSelectionChangedEventArgs? e = null)
{
_selectedIndicesCached = null;
_selectedItemsCached = null;
// Raise SelectionChanged event
if (e != null)
{
SelectionChanged?.Invoke(this, e);
}
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices));
if (_rootNode.Source != null)
{
RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems));
}
}
private void SelectImpl(int index, bool select)
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
var selected = _rootNode.Select(index, select);
if (selected)
{
AnchorIndex = new IndexPath(index);
}
}
private void SelectWithGroupImpl(int groupIndex, int itemIndex, bool select)
{
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
var childNode = _rootNode.GetAt(groupIndex, realizeChild: true);
var selected = childNode!.Select(itemIndex, select);
if (selected)
{
AnchorIndex = new IndexPath(groupIndex, itemIndex);
}
}
private void SelectWithPathImpl(IndexPath index, bool select)
{
bool selected = false;
if (_singleSelect)
{
ClearSelection(resetAnchor: true);
}
SelectionTreeHelper.TraverseIndexPath(
_rootNode,
index,
true,
(currentNode, path, depth, childIndex) =>
{
if (depth == path.GetSize() - 1)
{
selected = currentNode.Select(childIndex, select);
}
}
);
if (selected)
{
AnchorIndex = index;
}
}
private void SelectRangeFromAnchorImpl(int index, bool select)
{
int anchorIndex = 0;
var anchor = AnchorIndex;
if (anchor != null)
{
anchorIndex = anchor.GetAt(0);
}
_rootNode.SelectRange(new IndexRange(anchorIndex, index), select);
}
private void SelectRangeFromAnchorWithGroupImpl(int endGroupIndex, int endItemIndex, bool select)
{
var startGroupIndex = 0;
var startItemIndex = 0;
var anchorIndex = AnchorIndex;
if (anchorIndex != null)
{
startGroupIndex = anchorIndex.GetAt(0);
startItemIndex = anchorIndex.GetAt(1);
}
// Make sure start > end
if (startGroupIndex > endGroupIndex ||
(startGroupIndex == endGroupIndex && startItemIndex > endItemIndex))
{
int temp = startGroupIndex;
startGroupIndex = endGroupIndex;
endGroupIndex = temp;
temp = startItemIndex;
startItemIndex = endItemIndex;
endItemIndex = temp;
}
for (int groupIdx = startGroupIndex; groupIdx <= endGroupIndex; groupIdx++)
{
var groupNode = _rootNode.GetAt(groupIdx, realizeChild: true)!;
int startIndex = groupIdx == startGroupIndex ? startItemIndex : 0;
int endIndex = groupIdx == endGroupIndex ? endItemIndex : groupNode.DataCount - 1;
groupNode.SelectRange(new IndexRange(startIndex, endIndex), select);
}
}
private void SelectRangeImpl(IndexPath start, IndexPath end, bool select)
{
var winrtStart = start;
var winrtEnd = end;
// Make sure start <= end
if (winrtEnd.CompareTo(winrtStart) == -1)
{
var temp = winrtStart;
winrtStart = winrtEnd;
winrtEnd = temp;
}
// Note: Since we do not know the depth of the tree, we have to walk to each leaf
SelectionTreeHelper.TraverseRangeRealizeChildren(
_rootNode,
winrtStart,
winrtEnd,
info =>
{
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
});
}
private void BeginOperation()
{
if (_operationCount++ == 0)
{
_rootNode.BeginOperation();
}
}
private void EndOperation()
{
if (_operationCount == 0)
{
throw new AvaloniaInternalException("No selection operation in progress.");
}
SelectionModelSelectionChangedEventArgs? e = null;
if (--_operationCount == 0)
{
var changes = new List<SelectionNodeOperation>();
_rootNode.EndOperation(changes);
if (changes.Count > 0)
{
var changeSet = new SelectionModelChangeSet(changes);
e = changeSet.CreateEventArgs();
}
}
OnSelectionChanged(e);
_rootNode.Cleanup();
}
private void ApplyAutoSelect()
{
if (AutoSelect)
{
_selectedIndicesCached = null;
if (SelectedIndex == default && _rootNode.ItemsSourceView?.Count > 0)
{
using var operation = new Operation(this);
SelectImpl(0, true);
}
}
}
internal class SelectedItemInfo : ISelectedItemInfo
{
public SelectedItemInfo(SelectionNode node, IndexPath path)
{
Node = node;
Path = path;
}
public SelectionNode Node { get; }
public IndexPath Path { get; }
public int Count => Node.SelectedCount;
}
private struct Operation : IDisposable
{
private readonly SelectionModel _manager;
public Operation(SelectionModel manager) => (_manager = manager).BeginOperation();
public void Dispose() => _manager.EndOperation();
}
}
}

170
src/Avalonia.Controls/SelectionModelChangeSet.cs

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
internal class SelectionModelChangeSet
{
private readonly List<SelectionNodeOperation> _changes;
public SelectionModelChangeSet(List<SelectionNodeOperation> changes)
{
_changes = changes;
}
public SelectionModelSelectionChangedEventArgs CreateEventArgs()
{
var deselectedIndexCount = 0;
var selectedIndexCount = 0;
var deselectedItemCount = 0;
var selectedItemCount = 0;
foreach (var change in _changes)
{
deselectedIndexCount += change.DeselectedCount;
selectedIndexCount += change.SelectedCount;
if (change.Items != null)
{
deselectedItemCount += change.DeselectedCount;
selectedItemCount += change.SelectedCount;
}
}
var deselectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes,
deselectedIndexCount,
GetDeselectedIndexAt);
var selectedIndices = new SelectedItems<IndexPath, SelectionNodeOperation>(
_changes,
selectedIndexCount,
GetSelectedIndexAt);
var deselectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes,
deselectedItemCount,
GetDeselectedItemAt);
var selectedItems = new SelectedItems<object?, SelectionNodeOperation>(
_changes,
selectedItemCount,
GetSelectedItemAt);
return new SelectionModelSelectionChangedEventArgs(
deselectedIndices,
selectedIndices,
deselectedItems,
selectedItems);
}
private IndexPath GetDeselectedIndexAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.DeselectedCount;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private IndexPath GetSelectedIndexAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.SelectedCount;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetIndexAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private object? GetDeselectedItemAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.DeselectedCount : 0;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.DeselectedRanges;
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private object? GetSelectedItemAt(
List<SelectionNodeOperation> infos,
int index)
{
static int GetCount(SelectionNodeOperation info) => info.Items != null ? info.SelectedCount : 0;
static List<IndexRange>? GetRanges(SelectionNodeOperation info) => info.SelectedRanges;
return GetItemAt(infos, index, x => GetCount(x), x => GetRanges(x));
}
private IndexPath GetIndexAt(
List<SelectionNodeOperation> infos,
int index,
Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
{
var currentIndex = 0;
IndexPath path = default;
foreach (var info in infos)
{
var currentCount = getCount(info);
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
path = info.Path.CloneWithChildIndex(targetIndex);
break;
}
currentIndex += currentCount;
}
return path;
}
private object? GetItemAt(
List<SelectionNodeOperation> infos,
int index,
Func<SelectionNodeOperation, int> getCount,
Func<SelectionNodeOperation, List<IndexRange>?> getRanges)
{
var currentIndex = 0;
object? item = null;
foreach (var info in infos)
{
var currentCount = getCount(info);
if (index >= currentIndex && index < currentIndex + currentCount)
{
int targetIndex = GetIndexAt(getRanges(info), index - currentIndex);
item = info.Items?.GetAt(targetIndex);
break;
}
currentIndex += currentCount;
}
return item;
}
private int GetIndexAt(List<IndexRange>? ranges, int index)
{
var currentIndex = 0;
if (ranges != null)
{
foreach (var range in ranges)
{
var currentCount = (range.End - range.Begin) + 1;
if (index >= currentIndex && index < currentIndex + currentCount)
{
return range.Begin + (index - currentIndex);
}
currentIndex += currentCount;
}
}
throw new IndexOutOfRangeException();
}
}
}

83
src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs

@ -0,0 +1,83 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Provides data for the <see cref="SelectionModel.ChildrenRequested"/> event.
/// </summary>
public class SelectionModelChildrenRequestedEventArgs : EventArgs
{
private object? _source;
private IndexPath _sourceIndexPath;
private bool _throwOnAccess;
internal SelectionModelChildrenRequestedEventArgs(
object source,
IndexPath sourceIndexPath,
bool throwOnAccess)
{
source = source ?? throw new ArgumentNullException(nameof(source));
Initialize(source, sourceIndexPath, throwOnAccess);
}
/// <summary>
/// Gets or sets an observable which produces the children of the <see cref="Source"/>
/// object.
/// </summary>
public IObservable<object?>? Children { get; set; }
/// <summary>
/// Gets the object whose children are being requested.
/// </summary>
public object Source
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _source!;
}
}
/// <summary>
/// Gets the index of the object whose children are being requested.
/// </summary>
public IndexPath SourceIndex
{
get
{
if (_throwOnAccess)
{
throw new ObjectDisposedException(nameof(SelectionModelChildrenRequestedEventArgs));
}
return _sourceIndexPath;
}
}
internal void Initialize(
object? source,
IndexPath sourceIndexPath,
bool throwOnAccess)
{
if (!throwOnAccess && source == null)
{
throw new ArgumentNullException(nameof(source));
}
_source = source;
_sourceIndexPath = sourceIndexPath;
_throwOnAccess = throwOnAccess;
}
}
}

47
src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs

@ -0,0 +1,47 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
public class SelectionModelSelectionChangedEventArgs : EventArgs
{
public SelectionModelSelectionChangedEventArgs(
IReadOnlyList<IndexPath>? deselectedIndices,
IReadOnlyList<IndexPath>? selectedIndices,
IReadOnlyList<object?>? deselectedItems,
IReadOnlyList<object?>? selectedItems)
{
DeselectedIndices = deselectedIndices ?? Array.Empty<IndexPath>();
SelectedIndices = selectedIndices ?? Array.Empty<IndexPath>();
DeselectedItems = deselectedItems ?? Array.Empty<object?>();
SelectedItems= selectedItems ?? Array.Empty<object?>();
}
/// <summary>
/// Gets the indices of the items that were removed from the selection.
/// </summary>
public IReadOnlyList<IndexPath> DeselectedIndices { get; }
/// <summary>
/// Gets the indices of the items that were added to the selection.
/// </summary>
public IReadOnlyList<IndexPath> SelectedIndices { get; }
/// <summary>
/// Gets the items that were removed from the selection.
/// </summary>
public IReadOnlyList<object?> DeselectedItems { get; }
/// <summary>
/// Gets the items that were added to the selection.
/// </summary>
public IReadOnlyList<object?> SelectedItems { get; }
}
}

966
src/Avalonia.Controls/SelectionNode.cs

@ -0,0 +1,966 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Tracks nested selection.
/// </summary>
/// <remarks>
/// SelectionNode is the internal tree data structure that we keep track of for selection in
/// a nested scenario. This would map to one ItemsSourceView/Collection. This node reacts to
/// collection changes and keeps the selected indices up to date. This can either be a leaf
/// node or a non leaf node.
/// </remarks>
internal class SelectionNode : IDisposable
{
private readonly SelectionModel _manager;
private readonly List<SelectionNode?> _childrenNodes = new List<SelectionNode?>();
private readonly SelectionNode? _parent;
private readonly List<IndexRange> _selected = new List<IndexRange>();
private readonly List<int> _selectedIndicesCached = new List<int>();
private IDisposable? _childrenSubscription;
private SelectionNodeOperation? _operation;
private object? _source;
private bool _selectedIndicesCacheIsValid;
private bool _retainSelectionOnReset;
private List<object?>? _selectedItems;
public SelectionNode(SelectionModel manager, SelectionNode? parent)
{
_manager = manager;
_parent = parent;
}
public int AnchorIndex { get; set; } = -1;
public bool RetainSelectionOnReset
{
get => _retainSelectionOnReset;
set
{
if (_retainSelectionOnReset != value)
{
_retainSelectionOnReset = value;
if (_retainSelectionOnReset)
{
_selectedItems = new List<object?>();
PopulateSelectedItemsFromSelectedIndices();
}
else
{
_selectedItems = null;
}
foreach (var child in _childrenNodes)
{
if (child != null)
{
child.RetainSelectionOnReset = value;
}
}
}
}
}
public object? Source
{
get => _source;
set
{
if (_source != value)
{
if (_source != null)
{
ClearSelection();
ClearChildNodes();
UnhookCollectionChangedHandler();
}
_source = value;
// Setup ItemsSourceView
var newDataSource = value as ItemsSourceView;
if (value != null && newDataSource == null)
{
newDataSource = new ItemsSourceView((IEnumerable)value);
}
ItemsSourceView = newDataSource;
PopulateSelectedItemsFromSelectedIndices();
HookupCollectionChangedHandler();
OnSelectionChanged();
}
}
}
public ItemsSourceView? ItemsSourceView { get; private set; }
public int DataCount => ItemsSourceView?.Count ?? 0;
public int ChildrenNodeCount => _childrenNodes.Count;
public int RealizedChildrenNodeCount { get; private set; }
public IndexPath IndexPath
{
get
{
var path = new List<int>(); ;
var parent = _parent;
var child = this;
while (parent != null)
{
var childNodes = parent._childrenNodes;
var index = childNodes.IndexOf(child);
// We are walking up to the parent, so the path will be backwards
path.Insert(0, index);
child = parent;
parent = parent._parent;
}
return new IndexPath(path);
}
}
// For a genuine tree view, we dont know which node is leaf until we
// actually walk to it, so currently the tree builds up to the leaf. I don't
// create a bunch of leaf node instances - instead i use the same instance m_leafNode to avoid
// an explosion of node objects. However, I'm still creating the m_childrenNodes
// collection unfortunately.
public SelectionNode? GetAt(int index, bool realizeChild)
{
SelectionNode? child = null;
if (realizeChild)
{
if (ItemsSourceView == null || index < 0 || index >= ItemsSourceView.Count)
{
throw new IndexOutOfRangeException();
}
if (_childrenNodes.Count == 0)
{
if (ItemsSourceView != null)
{
for (int i = 0; i < ItemsSourceView.Count; i++)
{
_childrenNodes.Add(null);
}
}
}
if (_childrenNodes[index] == null)
{
var childData = ItemsSourceView!.GetAt(index);
IObservable<object?>? resolver = null;
if (childData != null)
{
var childDataIndexPath = IndexPath.CloneWithChildIndex(index);
resolver = _manager.ResolvePath(childData, childDataIndexPath);
}
if (resolver != null)
{
child = new SelectionNode(_manager, parent: this);
child.SetChildrenObservable(resolver);
}
else if (childData is IEnumerable<object> || childData is IList)
{
child = new SelectionNode(_manager, parent: this);
child.Source = childData;
}
else
{
child = _manager.SharedLeafNode;
}
if (_operation != null && child != _manager.SharedLeafNode)
{
child.BeginOperation();
}
_childrenNodes[index] = child;
RealizedChildrenNodeCount++;
}
else
{
child = _childrenNodes[index];
}
}
else
{
if (_childrenNodes.Count > 0)
{
child = _childrenNodes[index];
}
}
return child;
}
public void SetChildrenObservable(IObservable<object?> resolver)
{
_childrenSubscription = resolver.Subscribe(x => Source = x);
}
public int SelectedCount { get; private set; }
public bool IsSelected(int index)
{
var isSelected = false;
foreach (var range in _selected)
{
if (range.Contains(index))
{
isSelected = true;
break;
}
}
return isSelected;
}
// True -> Selected
// False -> Not Selected
// Null -> Some descendents are selected and some are not
public bool? IsSelectedWithPartial()
{
var isSelected = (bool?)false;
if (_parent != null)
{
var parentsChildren = _parent._childrenNodes;
var myIndexInParent = parentsChildren.IndexOf(this);
if (myIndexInParent != -1)
{
isSelected = _parent.IsSelectedWithPartial(myIndexInParent);
}
}
return isSelected;
}
// True -> Selected
// False -> Not Selected
// Null -> Some descendents are selected and some are not
public bool? IsSelectedWithPartial(int index)
{
SelectionState selectionState;
if (_childrenNodes.Count == 0 || // no nodes realized
_childrenNodes.Count <= index || // target node is not realized
_childrenNodes[index] == null || // target node is not realized
_childrenNodes[index] == _manager.SharedLeafNode) // target node is a leaf node.
{
// Ask parent if the target node is selected.
selectionState = IsSelected(index) ? SelectionState.Selected : SelectionState.NotSelected;
}
else
{
// targetNode is the node representing the index. This node is the parent.
// targetNode is a non-leaf node, containing one or many children nodes. Evaluate
// based on children of targetNode.
var targetNode = _childrenNodes[index];
selectionState = targetNode!.EvaluateIsSelectedBasedOnChildrenNodes();
}
return ConvertToNullableBool(selectionState);
}
public int SelectedIndex
{
get => SelectedCount > 0 ? SelectedIndices[0] : -1;
set
{
if (IsValidIndex(value) && (SelectedCount != 1 || !IsSelected(value)))
{
ClearSelection();
if (value != -1)
{
Select(value, true);
}
}
}
}
public List<int> SelectedIndices
{
get
{
if (!_selectedIndicesCacheIsValid)
{
_selectedIndicesCacheIsValid = true;
foreach (var range in _selected)
{
for (int index = range.Begin; index <= range.End; index++)
{
// Avoid duplicates
if (!_selectedIndicesCached.Contains(index))
{
_selectedIndicesCached.Add(index);
}
}
}
// Sort the list for easy consumption
_selectedIndicesCached.Sort();
}
return _selectedIndicesCached;
}
}
public IEnumerable<object> SelectedItems
{
get => SelectedIndices.Select(x => ItemsSourceView!.GetAt(x));
}
public void Dispose()
{
_childrenSubscription?.Dispose();
ItemsSourceView?.Dispose();
ClearChildNodes();
UnhookCollectionChangedHandler();
}
public void BeginOperation()
{
if (_operation != null)
{
throw new AvaloniaInternalException("Selection operation already in progress.");
}
_operation = new SelectionNodeOperation(this);
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.BeginOperation();
}
}
}
public void EndOperation(List<SelectionNodeOperation> changes)
{
if (_operation == null)
{
throw new AvaloniaInternalException("No selection operation in progress.");
}
if (_operation.HasChanges)
{
changes.Add(_operation);
}
_operation = null;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.EndOperation(changes);
}
}
}
public bool Cleanup()
{
var result = SelectedCount == 0;
for (var i = 0; i < _childrenNodes.Count; ++i)
{
var child = _childrenNodes[i];
if (child != null)
{
if (child.Cleanup())
{
child.Dispose();
_childrenNodes[i] = null;
}
else
{
result = false;
}
}
}
return result;
}
public bool Select(int index, bool select)
{
return Select(index, select, raiseOnSelectionChanged: true);
}
public bool ToggleSelect(int index)
{
return Select(index, !IsSelected(index));
}
public void SelectAll()
{
if (ItemsSourceView != null)
{
var size = ItemsSourceView.Count;
if (size > 0)
{
SelectRange(new IndexRange(0, size - 1), select: true);
}
}
}
public void Clear() => ClearSelection();
public bool SelectRange(IndexRange range, bool select)
{
if (IsValidIndex(range.Begin) && IsValidIndex(range.End))
{
if (select)
{
AddRange(range, raiseOnSelectionChanged: true);
}
else
{
RemoveRange(range, raiseOnSelectionChanged: true);
}
return true;
}
return false;
}
private void HookupCollectionChangedHandler()
{
if (ItemsSourceView != null)
{
ItemsSourceView.CollectionChanged += OnSourceListChanged;
}
}
private void UnhookCollectionChangedHandler()
{
if (ItemsSourceView != null)
{
ItemsSourceView.CollectionChanged -= OnSourceListChanged;
}
}
private bool IsValidIndex(int index)
{
return ItemsSourceView == null || (index >= 0 && index < ItemsSourceView.Count);
}
private void AddRange(IndexRange addRange, bool raiseOnSelectionChanged)
{
var selected = new List<IndexRange>();
SelectedCount += IndexRange.Add(_selected, addRange, selected);
if (selected.Count > 0)
{
_operation?.Selected(selected);
if (_selectedItems != null && ItemsSourceView != null)
{
for (var i = addRange.Begin; i <= addRange.End; ++i)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
}
}
private void RemoveRange(IndexRange removeRange, bool raiseOnSelectionChanged)
{
var removed = new List<IndexRange>();
SelectedCount -= IndexRange.Remove(_selected, removeRange, removed);
if (removed.Count > 0)
{
_operation?.Deselected(removed);
if (_selectedItems != null)
{
for (var i = removeRange.Begin; i <= removeRange.End; ++i)
{
_selectedItems.Remove(ItemsSourceView!.GetAt(i));
}
}
if (raiseOnSelectionChanged)
{
OnSelectionChanged();
}
}
}
private void ClearSelection()
{
// Deselect all items
if (_selected.Count > 0)
{
_operation?.Deselected(_selected);
_selected.Clear();
OnSelectionChanged();
}
_selectedItems?.Clear();
SelectedCount = 0;
AnchorIndex = -1;
}
private void ClearChildNodes()
{
foreach (var child in _childrenNodes)
{
if (child != null && child != _manager.SharedLeafNode)
{
child.Dispose();
}
}
RealizedChildrenNodeCount = 0;
}
private bool Select(int index, bool select, bool raiseOnSelectionChanged)
{
if (IsValidIndex(index))
{
// Ignore duplicate selection calls
if (IsSelected(index) == select)
{
return true;
}
var range = new IndexRange(index, index);
if (select)
{
AddRange(range, raiseOnSelectionChanged);
}
else
{
RemoveRange(range, raiseOnSelectionChanged);
}
return true;
}
return false;
}
private void OnSourceListChanged(object dataSource, NotifyCollectionChangedEventArgs args)
{
bool selectionInvalidated = false;
List<object?>? removed = null;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
selectionInvalidated = OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
case NotifyCollectionChangedAction.Remove:
{
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
break;
}
case NotifyCollectionChangedAction.Reset:
{
if (_selectedItems == null)
{
ClearSelection();
}
else
{
removed = RecreateSelectionFromSelectedItems();
}
selectionInvalidated = true;
break;
}
case NotifyCollectionChangedAction.Replace:
{
(selectionInvalidated, removed) = OnItemsRemoved(args.OldStartingIndex, args.OldItems);
selectionInvalidated |= OnItemsAdded(args.NewStartingIndex, args.NewItems.Count);
break;
}
}
if (selectionInvalidated)
{
OnSelectionChanged();
}
_manager.OnSelectionInvalidatedDueToCollectionChange(selectionInvalidated, removed);
}
private bool OnItemsAdded(int index, int count)
{
var selectionInvalidated = false;
// Update ranges for leaf items
var toAdd = new List<IndexRange>();
for (int i = 0; i < _selected.Count; i++)
{
var range = _selected[i];
// The range is after the inserted items, need to shift the range right
if (range.End >= index)
{
int begin = range.Begin;
// If the index left of newIndex is inside the range,
// Split the range and remember the left piece to add later
if (range.Contains(index - 1))
{
range.Split(index - 1, out var before, out _);
toAdd.Add(before);
begin = index;
}
// Shift the range to the right
_selected[i] = new IndexRange(begin + count, range.End + count);
selectionInvalidated = true;
}
}
// Add the left sides of the split ranges
_selected.AddRange(toAdd);
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
_childrenNodes.Insert(index, null);
}
}
// Adjust the anchor
if (AnchorIndex >= index)
{
AnchorIndex += count;
}
// Check if adding a node invalidated an ancestors
// selection state. For example if parent was selected before
// adding a new item makes the parent partially selected now.
if (!selectionInvalidated)
{
var parent = _parent;
while (parent != null)
{
var isSelected = parent.IsSelectedWithPartial();
// If a parent is selected, then it will become partially selected.
// If it is not selected or partially selected - there is no change.
if (isSelected == true)
{
selectionInvalidated = true;
break;
}
parent = parent._parent;
}
}
return selectionInvalidated;
}
private (bool, List<object?>) OnItemsRemoved(int index, IList items)
{
var selectionInvalidated = false;
var removed = new List<object?>();
var count = items.Count;
// Remove the items from the selection for leaf
if (ItemsSourceView!.Count > 0)
{
bool isSelected = false;
for (int i = 0; i <= count - 1; i++)
{
if (IsSelected(index + i))
{
isSelected = true;
removed.Add(items[i]);
}
}
if (isSelected)
{
var removeRange = new IndexRange(index, index + count - 1);
SelectedCount -= IndexRange.Remove(_selected, removeRange);
selectionInvalidated = true;
if (_selectedItems != null)
{
foreach (var i in items)
{
_selectedItems.Remove(i);
}
}
}
for (int i = 0; i < _selected.Count; i++)
{
var range = _selected[i];
// The range is after the removed items, need to shift the range left
if (range.End > index)
{
// Shift the range to the left
_selected[i] = new IndexRange(range.Begin - count, range.End - count);
selectionInvalidated = true;
}
}
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
if (_childrenNodes[index] != null)
{
removed.AddRange(_childrenNodes[index]!.SelectedItems);
RealizedChildrenNodeCount--;
_childrenNodes[index]!.Dispose();
}
_childrenNodes.RemoveAt(index);
}
}
//Adjust the anchor
if (AnchorIndex >= index)
{
AnchorIndex -= count;
}
}
else
{
// No more items in the list, clear
ClearSelection();
RealizedChildrenNodeCount = 0;
selectionInvalidated = true;
}
// Check if removing a node invalidated an ancestors
// selection state. For example if parent was partially selected before
// removing an item, it could be selected now.
if (!selectionInvalidated)
{
var parent = _parent;
while (parent != null)
{
var isSelected = parent.IsSelectedWithPartial();
// If a parent is partially selected, then it will become selected.
// If it is selected or not selected - there is no change.
if (!isSelected.HasValue)
{
selectionInvalidated = true;
break;
}
parent = parent._parent;
}
}
return (selectionInvalidated, removed);
}
private void OnSelectionChanged()
{
_selectedIndicesCacheIsValid = false;
_selectedIndicesCached.Clear();
}
public static bool? ConvertToNullableBool(SelectionState isSelected)
{
bool? result = null; // PartialySelected
if (isSelected == SelectionState.Selected)
{
result = true;
}
else if (isSelected == SelectionState.NotSelected)
{
result = false;
}
return result;
}
public SelectionState EvaluateIsSelectedBasedOnChildrenNodes()
{
var selectionState = SelectionState.NotSelected;
int realizedChildrenNodeCount = RealizedChildrenNodeCount;
int selectedCount = SelectedCount;
if (realizedChildrenNodeCount != 0 || selectedCount != 0)
{
// There are realized children or some selected leaves.
int dataCount = DataCount;
if (realizedChildrenNodeCount == 0 && selectedCount > 0)
{
// All nodes are leaves under it - we didn't create children nodes as an optimization.
// See if all/some or none of the leaves are selected.
selectionState = dataCount != selectedCount ?
SelectionState.PartiallySelected :
dataCount == selectedCount ? SelectionState.Selected : SelectionState.NotSelected;
}
else
{
// There are child nodes, walk them individually and evaluate based on each child
// being selected/not selected or partially selected.
selectedCount = 0;
int notSelectedCount = 0;
for (int i = 0; i < ChildrenNodeCount; i++)
{
var child = GetAt(i, realizeChild: false);
if (child != null)
{
// child is realized, ask it.
var isChildSelected = IsSelectedWithPartial(i);
if (isChildSelected == null)
{
selectionState = SelectionState.PartiallySelected;
break;
}
else if (isChildSelected == true)
{
selectedCount++;
}
else
{
notSelectedCount++;
}
}
else
{
// not realized.
if (IsSelected(i))
{
selectedCount++;
}
else
{
notSelectedCount++;
}
}
if (selectedCount > 0 && notSelectedCount > 0)
{
selectionState = SelectionState.PartiallySelected;
break;
}
}
if (selectionState != SelectionState.PartiallySelected)
{
if (selectedCount != 0 && selectedCount != dataCount)
{
selectionState = SelectionState.PartiallySelected;
}
else
{
selectionState = selectedCount == dataCount ? SelectionState.Selected : SelectionState.NotSelected;
}
}
}
}
return selectionState;
}
private void PopulateSelectedItemsFromSelectedIndices()
{
if (_selectedItems != null)
{
_selectedItems.Clear();
foreach (var i in SelectedIndices)
{
_selectedItems.Add(ItemsSourceView!.GetAt(i));
}
}
}
private List<object?> RecreateSelectionFromSelectedItems()
{
var removed = new List<object?>();
_selected.Clear();
SelectedCount = 0;
for (var i = 0; i < _selectedItems!.Count; ++i)
{
var item = _selectedItems[i];
var index = ItemsSourceView!.IndexOf(item);
if (index != -1)
{
IndexRange.Add(_selected, new IndexRange(index, index));
++SelectedCount;
}
else
{
removed.Add(item);
_selectedItems.RemoveAt(i--);
}
}
return removed;
}
public enum SelectionState
{
Selected,
NotSelected,
PartiallySelected
}
}
}

110
src/Avalonia.Controls/SelectionNodeOperation.cs

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls
{
internal class SelectionNodeOperation : ISelectedItemInfo
{
private readonly SelectionNode _owner;
private List<IndexRange>? _selected;
private List<IndexRange>? _deselected;
private int _selectedCount = -1;
private int _deselectedCount = -1;
public SelectionNodeOperation(SelectionNode owner)
{
_owner = owner;
}
public bool HasChanges => _selected?.Count > 0 || _deselected?.Count > 0;
public List<IndexRange>? SelectedRanges => _selected;
public List<IndexRange>? DeselectedRanges => _deselected;
public IndexPath Path => _owner.IndexPath;
public ItemsSourceView? Items => _owner.ItemsSourceView;
public int SelectedCount
{
get
{
if (_selectedCount == -1)
{
_selectedCount = (_selected != null) ? IndexRange.GetCount(_selected) : 0;
}
return _selectedCount;
}
}
public int DeselectedCount
{
get
{
if (_deselectedCount == -1)
{
_deselectedCount = (_deselected != null) ? IndexRange.GetCount(_deselected) : 0;
}
return _deselectedCount;
}
}
public void Selected(IndexRange range)
{
Add(range, ref _selected, _deselected);
_selectedCount = -1;
}
public void Selected(IEnumerable<IndexRange> ranges)
{
foreach (var range in ranges)
{
Selected(range);
}
}
public void Deselected(IndexRange range)
{
Add(range, ref _deselected, _selected);
_deselectedCount = -1;
}
public void Deselected(IEnumerable<IndexRange> ranges)
{
foreach (var range in ranges)
{
Deselected(range);
}
}
private static void Add(
IndexRange range,
ref List<IndexRange>? add,
List<IndexRange>? remove)
{
if (remove != null)
{
var removed = new List<IndexRange>();
IndexRange.Remove(remove, range, removed);
var selected = IndexRange.Subtract(range, removed);
if (selected.Any())
{
add ??= new List<IndexRange>();
foreach (var r in selected)
{
IndexRange.Add(add, r);
}
}
}
else
{
add ??= new List<IndexRange>();
IndexRange.Add(add, range);
}
}
}
}

2
src/Avalonia.Controls/TabControl.cs

@ -240,7 +240,7 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && e.Pointer.Type == PointerType.Mouse)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}

663
src/Avalonia.Controls/TreeView.cs

@ -2,11 +2,12 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
@ -42,15 +43,29 @@ namespace Avalonia.Controls
o => o.SelectedItems,
(o, v) => o.SelectedItems = v);
/// <summary>
/// Defines the <see cref="Selection"/> property.
/// </summary>
public static readonly DirectProperty<TreeView, ISelectionModel> SelectionProperty =
SelectingItemsControl.SelectionProperty.AddOwner<TreeView>(
o => o.Selection,
(o, v) => o.Selection = v);
/// <summary>
/// Defines the <see cref="SelectionMode"/> property.
/// </summary>
public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
ListBox.SelectionModeProperty.AddOwner<TreeView>();
private static readonly IList Empty = Array.Empty<object>();
/// <summary>
/// Defines the <see cref="SelectionChanged"/> property.
/// </summary>
public static RoutedEvent<SelectionChangedEventArgs> SelectionChangedEvent =
SelectingItemsControl.SelectionChangedEvent;
private object _selectedItem;
private IList _selectedItems;
private ISelectionModel _selection;
private readonly SelectedItemsSync _selectedItems;
/// <summary>
/// Initializes static members of the <see cref="TreeView"/> class.
@ -60,6 +75,13 @@ namespace Avalonia.Controls
// HACK: Needed or SelectedItem property will not be found in Release build.
}
public TreeView()
{
// Setting Selection to null causes a default SelectionModel to be created.
Selection = null;
_selectedItems = new SelectedItemsSync(Selection);
}
/// <summary>
/// Occurs when the control's selection changes.
/// </summary>
@ -84,8 +106,6 @@ namespace Avalonia.Controls
set => SetValue(AutoScrollToSelectedItemProperty, value);
}
private bool _syncingSelectedItems;
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
@ -95,61 +115,102 @@ namespace Avalonia.Controls
set => SetValue(SelectionModeProperty, value);
}
/// <summary>
/// Gets or sets the selected item.
/// </summary>
/// <summary>
/// Gets or sets the selected item.
/// </summary>
public object SelectedItem
{
get => _selectedItem;
set
{
var selectedItems = SelectedItems;
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
get => Selection.SelectedItem;
set => Selection.SelectedIndex = IndexFromItem(value);
}
if (value != null)
{
if (selectedItems.Count != 1 || selectedItems[0] != value)
{
_syncingSelectedItems = true;
SelectSingleItem(value);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
/// <summary>
/// Gets or sets the selected items.
/// </summary>
protected IList SelectedItems
{
get => _selectedItems.GetOrCreateItems();
set => _selectedItems.SetItems(value);
}
/// <summary>
/// Gets the selected items.
/// Gets or sets a model holding the current selection.
/// </summary>
public IList SelectedItems
public ISelectionModel Selection
{
get
get => _selection;
set
{
if (_selectedItems == null)
value ??= new SelectionModel
{
_selectedItems = new AvaloniaList<object>();
SubscribeToSelectedItems();
}
return _selectedItems;
}
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
RetainSelectionOnReset = true,
};
set
{
if (value?.IsFixedSize == true || value?.IsReadOnly == true)
if (_selection != value)
{
throw new NotSupportedException(
"Cannot use a fixed size or read-only collection as SelectedItems.");
}
if (value == null)
{
throw new ArgumentNullException(nameof(value), "Cannot set Selection to null.");
}
else if (value.Source != null && value.Source != Items)
{
throw new ArgumentException("Selection has invalid Source.");
}
List<object> oldSelection = null;
if (_selection != null)
{
oldSelection = Selection.SelectedItems.ToList();
_selection.PropertyChanged -= OnSelectionModelPropertyChanged;
_selection.SelectionChanged -= OnSelectionModelSelectionChanged;
_selection.ChildrenRequested -= OnSelectionModelChildrenRequested;
MarkContainersUnselected();
}
_selection = value;
if (_selection != null)
{
_selection.Source = Items;
_selection.PropertyChanged += OnSelectionModelPropertyChanged;
_selection.SelectionChanged += OnSelectionModelSelectionChanged;
_selection.ChildrenRequested += OnSelectionModelChildrenRequested;
UnsubscribeFromSelectedItems();
_selectedItems = value ?? new AvaloniaList<object>();
SubscribeToSelectedItems();
if (_selection.SingleSelect)
{
SelectionMode &= ~SelectionMode.Multiple;
}
else
{
SelectionMode |= SelectionMode.Multiple;
}
if (_selection.AutoSelect)
{
SelectionMode |= SelectionMode.AlwaysSelected;
}
else
{
SelectionMode &= ~SelectionMode.AlwaysSelected;
}
UpdateContainerSelection();
var selectedItem = SelectedItem;
if (_selectedItem != selectedItem)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, selectedItem);
_selectedItem = selectedItem;
}
}
}
}
}
@ -182,186 +243,12 @@ namespace Avalonia.Controls
/// Note that this method only selects nodes currently visible due to their parent nodes
/// being expanded: it does not expand nodes.
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
public void SelectAll() => Selection.SelectAll();
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
private void SubscribeToSelectedItems()
{
if (_selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged += SelectedItemsCollectionChanged;
}
SelectedItemsCollectionChanged(
_selectedItems,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void SelectSingleItem(object item)
{
SelectedItems.Clear();
SelectedItems.Add(item);
}
/// <summary>
/// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
IList added = null;
IList removed = null;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SelectedItemsAdded(e.NewItems.Cast<object>().ToArray());
if (AutoScrollToSelectedItem)
{
var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]);
container?.BringIntoView();
}
added = e.NewItems;
break;
case NotifyCollectionChangedAction.Remove:
if (!_syncingSelectedItems)
{
if (SelectedItems.Count == 0)
{
SelectedItem = null;
}
else
{
var selectedIndex = SelectedItems.IndexOf(_selectedItem);
if (selectedIndex == -1)
{
var old = _selectedItem;
_selectedItem = SelectedItems[0];
RaisePropertyChanged(SelectedItemProperty, old, _selectedItem);
}
}
}
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
removed = e.OldItems;
break;
case NotifyCollectionChangedAction.Reset:
foreach (IControl container in ItemContainerGenerator.Index.Containers)
{
MarkContainerSelected(container, false);
}
if (SelectedItems.Count > 0)
{
SelectedItemsAdded(SelectedItems);
added = SelectedItems;
}
else if (!_syncingSelectedItems)
{
SelectedItem = null;
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
{
MarkItemSelected(item, false);
}
foreach (var item in e.NewItems)
{
MarkItemSelected(item, true);
}
if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
{
var oldItem = SelectedItem;
var item = SelectedItems[0];
_selectedItem = item;
RaisePropertyChanged(SelectedItemProperty, oldItem, item);
}
added = e.NewItems;
removed = e.OldItems;
break;
}
if (added?.Count > 0 || removed?.Count > 0)
{
var changed = new SelectionChangedEventArgs(
SelectingItemsControl.SelectionChangedEvent,
removed ?? Empty,
added ?? Empty);
RaiseEvent(changed);
}
}
private void MarkItemSelected(object item, bool selected)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(item);
MarkContainerSelected(container, selected);
}
private void SelectedItemsAdded(IList items)
{
if (items.Count == 0)
{
return;
}
foreach (object item in items)
{
MarkItemSelected(item, true);
}
if (SelectedItem == null && !_syncingSelectedItems)
{
SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]);
}
}
/// <summary>
/// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
private void UnsubscribeFromSelectedItems()
{
if (_selectedItems is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= SelectedItemsCollectionChanged;
}
}
public void UnselectAll() => Selection.ClearSelection();
(bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element,
NavigationDirection direction)
@ -445,6 +332,72 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when <see cref="SelectionModel.PropertyChanged"/> is raised.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SelectionModel.AnchorIndex) && AutoScrollToSelectedItem)
{
var container = ContainerFromIndex(Selection.AnchorIndex);
if (container != null)
{
container.BringIntoView();
}
}
}
/// <summary>
/// Called when <see cref="SelectionModel.SelectionChanged"/> is raised.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void OnSelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
void Mark(IndexPath index, bool selected)
{
var container = ContainerFromIndex(index);
if (container != null)
{
MarkContainerSelected(container, selected);
}
}
foreach (var i in e.SelectedIndices)
{
Mark(i, true);
}
foreach (var i in e.DeselectedIndices)
{
Mark(i, false);
}
var newSelectedItem = SelectedItem;
if (newSelectedItem != _selectedItem)
{
RaisePropertyChanged(SelectedItemProperty, _selectedItem, newSelectedItem);
_selectedItem = newSelectedItem;
}
var ev = new SelectionChangedEventArgs(
SelectionChangedEvent,
e.DeselectedItems.ToList(),
e.SelectedItems.ToList());
RaiseEvent(ev);
}
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl;
e.Children = container?.GetObservable(ItemsProperty);
}
private TreeViewItem GetContainerInDirection(
TreeViewItem from,
NavigationDirection direction,
@ -498,6 +451,12 @@ namespace Avalonia.Controls
return result;
}
protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
Selection.Source = Items;
base.ItemsChanged(e);
}
/// <inheritdoc/>
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@ -519,6 +478,18 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
if (property == SelectionModeProperty)
{
var mode = newValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}
}
/// <summary>
/// Updates the selection for an item based on user interaction.
/// </summary>
@ -534,9 +505,9 @@ namespace Avalonia.Controls
bool toggleModifier = false,
bool rightButton = false)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
var index = IndexFromContainer((TreeViewItem)container);
if (item == null)
if (index.GetSize() == 0)
{
return;
}
@ -553,41 +524,48 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
if (rightButton)
if (!select)
{
Selection.DeselectAt(index);
}
else if (rightButton)
{
if (!SelectedItems.Contains(item))
if (!Selection.IsSelectedAt(index))
{
SelectSingleItem(item);
Selection.SelectedIndex = index;
}
}
else if (!toggle && !range)
{
SelectSingleItem(item);
Selection.SelectedIndex = index;
}
else if (multi && range)
{
SynchronizeItems(
SelectedItems,
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
using var operation = Selection.Update();
var anchor = Selection.AnchorIndex;
if (anchor.GetSize() == 0)
{
anchor = new IndexPath(0);
}
Selection.ClearSelection();
Selection.AnchorIndex = anchor;
Selection.SelectRangeFromAnchorTo(index);
}
else
{
var i = SelectedItems.IndexOf(item);
if (i != -1)
if (Selection.IsSelectedAt(index))
{
SelectedItems.Remove(item);
Selection.DeselectAt(index);
}
else if (multi)
{
Selection.SelectAt(index);
}
else
{
if (multi)
{
SelectedItems.Add(item);
}
else
{
SelectedItem = item;
}
Selection.SelectedIndex = index;
}
}
}
@ -610,117 +588,6 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Find which node is first in hierarchy.
/// </summary>
/// <param name="treeView">Search root.</param>
/// <param name="nodeA">Nodes to find.</param>
/// <param name="nodeB">Node to find.</param>
/// <returns>Found first node.</returns>
private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB)
{
return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
}
private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator,
TreeViewItem nodeA,
TreeViewItem nodeB)
{
IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
foreach (ItemContainerInfo container in containers)
{
TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
if (node != null)
{
return node;
}
}
return null;
}
private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB)
{
if (node == null)
{
return null;
}
TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null;
if (match != null)
{
return match;
}
return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB);
}
/// <summary>
/// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
/// The range is inclusive.
/// </summary>
/// <param name="from">From container.</param>
/// <param name="to">To container.</param>
private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to)
{
var items = new List<object>();
if (from == null || to == null)
{
return items;
}
TreeViewItem firstItem = FindFirstNode(this, from, to);
if (firstItem == null)
{
return items;
}
bool wasReversed = false;
if (firstItem == to)
{
var temp = from;
from = to;
to = temp;
wasReversed = true;
}
TreeViewItem node = from;
while (node != to)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(node);
if (item != null)
{
items.Add(item);
}
node = GetContainerInDirection(node, NavigationDirection.Down, true);
}
var toItem = ItemContainerGenerator.Index.ItemFromContainer(to);
if (toItem != null)
{
items.Add(toItem);
}
if (wasReversed)
{
items.Reverse();
}
return items;
}
/// <summary>
/// Updates the selection based on an event that may have originated in a container that
/// belongs to the control.
@ -826,26 +693,90 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Makes a list of objects equal another (though doesn't preserve order).
/// </summary>
/// <param name="items">The items collection.</param>
/// <param name="desired">The desired items.</param>
private static void SynchronizeItems(IList items, IEnumerable<object> desired)
private void MarkContainersUnselected()
{
var list = items.Cast<object>().ToList();
var toRemove = list.Except(desired).ToList();
var toAdd = desired.Except(list).ToList();
foreach (var container in ItemContainerGenerator.Index.Containers)
{
MarkContainerSelected(container, false);
}
}
private void UpdateContainerSelection()
{
var index = ItemContainerGenerator.Index;
foreach (var container in index.Containers)
{
var i = IndexFromContainer((TreeViewItem)container);
MarkContainerSelected(
container,
Selection.IsSelectedAt(i) != false);
}
}
private static IndexPath IndexFromContainer(TreeViewItem container)
{
var result = new List<int>();
while (true)
{
if (container.Level == 0)
{
var treeView = container.FindAncestorOfType<TreeView>();
if (treeView == null)
{
return default;
}
result.Add(treeView.ItemContainerGenerator.IndexFromContainer(container));
result.Reverse();
return new IndexPath(result);
}
else
{
var parent = container.FindAncestorOfType<TreeViewItem>();
if (parent == null)
{
return default;
}
result.Add(parent.ItemContainerGenerator.IndexFromContainer(container));
container = parent;
}
}
}
private IndexPath IndexFromItem(object item)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(item) as TreeViewItem;
foreach (var i in toRemove)
if (container != null)
{
items.Remove(i);
return IndexFromContainer(container);
}
foreach (var i in toAdd)
return default;
}
private TreeViewItem ContainerFromIndex(IndexPath index)
{
TreeViewItem treeViewItem = null;
for (var i = 0; i < index.GetSize(); ++i)
{
items.Add(i);
var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator;
treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem;
if (treeViewItem == null)
{
return null;
}
}
return treeViewItem;
}
}
}

46
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -1,17 +1,30 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Controls.Utils
{
internal class BorderRenderHelper
{
private bool _useComplexRendering;
private bool? _backendSupportsIndividualCorners;
private StreamGeometry _backgroundGeometryCache;
private StreamGeometry _borderGeometryCache;
private Size _size;
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private bool _initialized;
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
{
if (borderThickness.IsUniform && cornerRadius.IsUniform)
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
.SupportsIndividualRoundRects;
_size = finalSize;
_borderThickness = borderThickness;
_cornerRadius = cornerRadius;
_initialized = true;
if (borderThickness.IsUniform && (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true))
{
_backgroundGeometryCache = null;
_borderGeometryCache = null;
@ -67,7 +80,19 @@ namespace Avalonia.Controls.Utils
}
}
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush background, IBrush borderBrush, BoxShadows boxShadows)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows);
}
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows)
{
if (_useComplexRendering)
{
@ -85,9 +110,7 @@ namespace Avalonia.Controls.Utils
}
else
{
var borderThickness = borders.Top;
var top = borderThickness * 0.5;
var borderThickness = _borderThickness.Top;
IPen pen = null;
if (borderThickness > 0)
@ -95,11 +118,16 @@ namespace Avalonia.Controls.Utils
pen = new Pen(borderBrush, borderThickness);
}
var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness);
var rrect = new RoundedRect(new Rect(_size), _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft);
if (Math.Abs(borderThickness) > double.Epsilon)
{
rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5);
}
context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft);
context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows);
}
}
}
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints)
{

227
src/Avalonia.Controls/Utils/SelectedItemsSync.cs

@ -0,0 +1,227 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Synchronizes an <see cref="ISelectionModel"/> with a list of SelectedItems.
/// </summary>
internal class SelectedItemsSync
{
private IList? _items;
private bool _updatingItems;
private bool _updatingModel;
public SelectedItemsSync(ISelectionModel model)
{
model = model ?? throw new ArgumentNullException(nameof(model));
Model = model;
}
public ISelectionModel Model { get; private set; }
public IList GetOrCreateItems()
{
if (_items == null)
{
var items = new AvaloniaList<object>(Model.SelectedItems);
items.CollectionChanged += ItemsCollectionChanged;
Model.SelectionChanged += SelectionModelSelectionChanged;
_items = items;
}
return _items;
}
public void SetItems(IList? items)
{
items ??= new AvaloniaList<object>();
if (items.IsFixedSize)
{
throw new NotSupportedException(
"Cannot assign fixed size selection to SelectedItems.");
}
if (_items is INotifyCollectionChanged incc)
{
incc.CollectionChanged -= ItemsCollectionChanged;
}
if (_items == null)
{
Model.SelectionChanged += SelectionModelSelectionChanged;
}
try
{
_updatingModel = true;
_items = items;
using (Model.Update())
{
Model.ClearSelection();
Add(items);
}
if (_items is INotifyCollectionChanged incc2)
{
incc2.CollectionChanged += ItemsCollectionChanged;
}
}
finally
{
_updatingModel = false;
}
}
public void SetModel(ISelectionModel model)
{
model = model ?? throw new ArgumentNullException(nameof(model));
if (_items != null)
{
Model.SelectionChanged -= SelectionModelSelectionChanged;
Model = model;
Model.SelectionChanged += SelectionModelSelectionChanged;
try
{
_updatingItems = true;
_items.Clear();
foreach (var i in model.SelectedItems)
{
_items.Add(i);
}
}
finally
{
_updatingItems = false;
}
}
}
private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (_updatingItems)
{
return;
}
if (_items == null)
{
throw new AvaloniaInternalException("CollectionChanged raised but we don't have items.");
}
void Remove()
{
foreach (var i in e.OldItems)
{
var index = IndexOf(Model.Source, i);
if (index != -1)
{
Model.Deselect(index);
}
}
}
try
{
using var operation = Model.Update();
_updatingModel = true;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
Remove();
break;
case NotifyCollectionChangedAction.Replace:
Remove();
Add(e.NewItems);
break;
case NotifyCollectionChangedAction.Reset:
Model.ClearSelection();
Add(_items);
break;
}
}
finally
{
_updatingModel = false;
}
}
private void Add(IList newItems)
{
foreach (var i in newItems)
{
var index = IndexOf(Model.Source, i);
if (index != -1)
{
Model.Select(index);
}
}
}
private void SelectionModelSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
if (_updatingModel)
{
return;
}
if (_items == null)
{
throw new AvaloniaInternalException("SelectionModelChanged raised but we don't have items.");
}
try
{
var deselected = e.DeselectedItems.ToList();
var selected = e.SelectedItems.ToList();
_updatingItems = true;
foreach (var i in deselected)
{
_items.Remove(i);
}
foreach (var i in selected)
{
_items.Add(i);
}
}
finally
{
_updatingItems = false;
}
}
private static int IndexOf(object source, object item)
{
if (source is IList l)
{
return l.IndexOf(item);
}
else if (source is ItemsSourceView v)
{
return v.IndexOf(item);
}
return -1;
}
}
}

189
src/Avalonia.Controls/Utils/SelectionTreeHelper.cs

@ -0,0 +1,189 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace Avalonia.Controls.Utils
{
internal static class SelectionTreeHelper
{
public static void TraverseIndexPath(
SelectionNode root,
IndexPath path,
bool realizeChildren,
Action<SelectionNode, IndexPath, int, int> nodeAction)
{
var node = root;
for (int depth = 0; depth < path.GetSize(); depth++)
{
int childIndex = path.GetAt(depth);
nodeAction(node, path, depth, childIndex);
if (depth < path.GetSize() - 1)
{
node = node.GetAt(childIndex, realizeChildren)!;
}
}
}
public static void Traverse(
SelectionNode root,
bool realizeChildren,
Action<TreeWalkNodeInfo> nodeAction)
{
var pendingNodes = new List<TreeWalkNodeInfo>();
var current = new IndexPath(null);
pendingNodes.Add(new TreeWalkNodeInfo(root, current));
while (pendingNodes.Count > 0)
{
var nextNode = pendingNodes.Last();
pendingNodes.RemoveAt(pendingNodes.Count - 1);
int count = realizeChildren ? nextNode.Node.DataCount : nextNode.Node.ChildrenNodeCount;
for (int i = count - 1; i >= 0; i--)
{
var child = nextNode.Node.GetAt(i, realizeChildren);
var childPath = nextNode.Path.CloneWithChildIndex(i);
if (child != null)
{
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, nextNode.Node));
}
}
// Queue the children first and then perform the action. This way
// the action can remove the children in the action if necessary
nodeAction(nextNode);
}
}
public static void TraverseRangeRealizeChildren(
SelectionNode root,
IndexPath start,
IndexPath end,
Action<TreeWalkNodeInfo> nodeAction)
{
var pendingNodes = new List<TreeWalkNodeInfo>();
var current = start;
// Build up the stack to account for the depth first walk up to the
// start index path.
TraverseIndexPath(
root,
start,
true,
(node, path, depth, childIndex) =>
{
var currentPath = StartPath(path, depth);
bool isStartPath = IsSubSet(start, currentPath);
bool isEndPath = IsSubSet(end, currentPath);
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--)
{
var child = node.GetAt(i, realizeChild: true);
if (child != null)
{
var childPath = currentPath.CloneWithChildIndex(i);
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, node));
}
}
});
// From the start index path, do a depth first walk as long as the
// current path is less than the end path.
while (pendingNodes.Count > 0)
{
var info = pendingNodes.Last();
pendingNodes.RemoveAt(pendingNodes.Count - 1);
int depth = info.Path.GetSize();
bool isStartPath = IsSubSet(start, info.Path);
bool isEndPath = IsSubSet(end, info.Path);
int startIndex = depth < start.GetSize() && isStartPath ? start.GetAt(depth) : 0;
int endIndex = depth < end.GetSize() && isEndPath ? end.GetAt(depth) : info.Node.DataCount - 1;
for (int i = endIndex; i >= startIndex; i--)
{
var child = info.Node.GetAt(i, realizeChild: true);
if (child != null)
{
var childPath = info.Path.CloneWithChildIndex(i);
pendingNodes.Add(new TreeWalkNodeInfo(child, childPath, info.Node));
}
}
nodeAction(info);
if (info.Path.CompareTo(end) == 0)
{
// We reached the end index path. stop iterating.
break;
}
}
}
private static bool IsSubSet(IndexPath path, IndexPath subset)
{
var subsetSize = subset.GetSize();
if (path.GetSize() < subsetSize)
{
return false;
}
for (int i = 0; i < subsetSize; i++)
{
if (path.GetAt(i) != subset.GetAt(i))
{
return false;
}
}
return true;
}
private static IndexPath StartPath(IndexPath path, int length)
{
var subPath = new List<int>();
for (int i = 0; i < length; i++)
{
subPath.Add(path.GetAt(i));
}
return new IndexPath(subPath);
}
public struct TreeWalkNodeInfo
{
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath, SelectionNode? parent)
{
node = node ?? throw new ArgumentNullException(nameof(node));
Node = node;
Path = indexPath;
ParentNode = parent;
}
public TreeWalkNodeInfo(SelectionNode node, IndexPath indexPath)
{
node = node ?? throw new ArgumentNullException(nameof(node));
Node = node;
Path = indexPath;
ParentNode = null;
}
public SelectionNode Node { get; }
public IndexPath Path { get; }
public SelectionNode? ParentNode { get; }
};
}
}

2
src/Avalonia.Controls/Window.cs

@ -675,7 +675,9 @@ namespace Avalonia.Controls
if (o != n)
{
#pragma warning disable CS0618 // Type or member is obsolete
RaisePropertyChanged(HasSystemDecorationsProperty, o, n);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

5
src/Avalonia.Controls/WindowState.cs

@ -19,5 +19,10 @@ namespace Avalonia.Controls
/// The window is maximized.
/// </summary>
Maximized,
/// <summary>
/// The window is fullscreen.
/// </summary>
FullScreen,
}
}

3
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -8,8 +8,11 @@
-->
<Version>0.7.0</Version>
<NoWarn>CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Remote\HtmlTransport\webapp\build\**\*.gz" />
<EmbeddedResource Condition="'$(Configuration)' == 'Debug'" Remove="Remote\HtmlTransport\webapp\build\**\*.map.gz"/>
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

4
src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs

@ -35,5 +35,7 @@ namespace Avalonia.DesignerSupport.Remote
add {}
remove {}
}
public void Start() => _inner?.Start();
}
}
}

90
src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs

@ -0,0 +1,90 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod
{
private string _path;
private string _lastContents;
private bool _disposed;
public FileWatcherTransport(Uri file)
{
_path = file.LocalPath;
}
public void Dispose()
{
_disposed = true;
}
void Dump(object o, string pad)
{
foreach (var p in o.GetType().GetProperties())
{
Console.Write($"{pad}{p.Name}: ");
var v = p.GetValue(o);
if (v == null || v.GetType().IsPrimitive || v is string || v is Guid)
Console.WriteLine(v);
else
{
Console.WriteLine();
Dump(v, pad + " ");
}
}
}
public Task Send(object data)
{
Console.WriteLine(data.GetType().Name);
Dump(data, " ");
return Task.CompletedTask;
}
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
{
add
{
_onMessage+=value;
}
remove { _onMessage -= value; }
}
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
public void Start()
{
UpdaterThread();
}
// I couldn't get FileSystemWatcher working on Linux, so I came up with this abomination
async void UpdaterThread()
{
while (!_disposed)
{
var data = File.ReadAllText(_path);
if (data != _lastContents)
{
Console.WriteLine("Triggering XAML update");
_lastContents = data;
_onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data });
}
await Task.Delay(100);
}
}
public string PreviewerMethod => RemoteDesignerEntryPoint.Methods.Html;
}
interface ITransportWithEnforcedMethod
{
string PreviewerMethod { get; }
}
}

266
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Viewport;
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
{
public class HtmlWebSocketTransport : IAvaloniaRemoteTransportConnection
{
private readonly IAvaloniaRemoteTransportConnection _signalTransport;
private readonly SimpleWebSocketHttpServer _simpleServer;
private readonly Dictionary<string, byte[]> _resources;
private SimpleWebSocket _pendingSocket;
private bool _disposed;
private object _lock = new object();
private AutoResetEvent _wakeup = new AutoResetEvent(false);
private FrameMessage _lastFrameMessage = null;
private FrameMessage _lastSentFrameMessage = null;
private RequestViewportResizeMessage _lastViewportRequest;
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException;
private static readonly Dictionary<string, string> Mime = new Dictionary<string, string>
{
["html"] = "text/html", ["htm"] = "text/html", ["js"] = "text/javascript", ["css"] = "text/css"
};
private static readonly byte[] NotFound = Encoding.UTF8.GetBytes("404 - Not Found");
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
{
if (listenUri.Scheme != "http")
throw new ArgumentException("listenUri");
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary(
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
r =>
{
using (var s =
new GZipStream(typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceStream(r),
CompressionMode.Decompress))
{
var ms = new MemoryStream();
s.CopyTo(ms);
return ms.ToArray();
}
});
_signalTransport = signalTransport;
var address = IPAddress.Parse(listenUri.Host);
_simpleServer = new SimpleWebSocketHttpServer(address, listenUri.Port);
_simpleServer.Listen();
Task.Run(AcceptWorker);
Task.Run(SocketWorker);
_signalTransport.Send(new HtmlTransportStartedMessage { Uri = "http://" + address + ":" + listenUri.Port + "/" });
}
async void AcceptWorker()
{
while (true)
{
using (var req = await _simpleServer.AcceptAsync())
{
if (!req.IsWebsocketRequest)
{
var key = req.Path == "/" ? "index.html" : req.Path.TrimStart('/').Replace('/', '.');
if (_resources.TryGetValue(key, out var data))
{
var ext = Path.GetExtension(key).Substring(1);
string mime = null;
if (ext == null || !Mime.TryGetValue(ext, out mime))
mime = "application/octet-stream";
await req.RespondAsync(200, data, mime);
}
else
{
await req.RespondAsync(404, NotFound, "text/plain");
}
}
else
{
var socket = await req.AcceptWebSocket();
SocketReceiveWorker(socket);
lock (_lock)
{
_pendingSocket?.Dispose();
_pendingSocket = socket;
}
}
}
}
}
async void SocketReceiveWorker(SimpleWebSocket socket)
{
try
{
while (true)
{
var msg = await socket.ReceiveMessage().ConfigureAwait(false);
if(msg == null)
return;
if (msg.IsText)
{
var s = Encoding.UTF8.GetString(msg.Data);
var parts = s.Split(':');
if (parts[0] == "frame-received")
_onMessage?.Invoke(this, new FrameReceivedMessage { SequenceId = long.Parse(parts[1]) });
}
}
}
catch(Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
async void SocketWorker()
{
try
{
SimpleWebSocket socket = null;
while (true)
{
if (_disposed)
{
socket?.Dispose();
return;
}
FrameMessage sendNow = null;
lock (_lock)
{
if (_pendingSocket != null)
{
socket?.Dispose();
socket = _pendingSocket;
_pendingSocket = null;
_lastSentFrameMessage = null;
}
if (_lastFrameMessage != _lastSentFrameMessage)
_lastSentFrameMessage = sendNow = _lastFrameMessage;
}
if (sendNow != null && socket != null)
{
await socket.SendMessage(
$"frame:{sendNow.SequenceId}:{sendNow.Width}:{sendNow.Height}:{sendNow.Stride}:{sendNow.DpiX}:{sendNow.DpiY}");
await socket.SendMessage(false, sendNow.Data);
}
_wakeup.WaitOne(TimeSpan.FromSeconds(1));
}
}
catch(Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
public void Dispose()
{
_pendingSocket?.Dispose();
_simpleServer.Dispose();
}
public Task Send(object data)
{
if (data is FrameMessage frame)
{
_lastFrameMessage = frame;
_wakeup.Set();
return Task.CompletedTask;
}
if (data is RequestViewportResizeMessage req)
{
return Task.CompletedTask;
}
return _signalTransport.Send(data);
}
public void Start()
{
_onMessage?.Invoke(this, new Avalonia.Remote.Protocol.Viewport.ClientSupportedPixelFormatsMessage
{
Formats = new []{PixelFormat.Rgba8888}
});
_signalTransport.Start();
}
#region Forward
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
{
add
{
bool subscribeToInner;
lock (_lock)
{
subscribeToInner = _onMessage == null;
_onMessage += value;
}
if (subscribeToInner)
_signalTransport.OnMessage += OnSignalTransportMessage;
}
remove
{
lock (_lock)
{
_onMessage -= value;
if (_onMessage == null)
_signalTransport.OnMessage -= OnSignalTransportMessage;
}
}
}
private void OnSignalTransportMessage(IAvaloniaRemoteTransportConnection signal, object message) => _onMessage?.Invoke(this, message);
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException
{
add
{
lock (_lock)
{
var subscribeToInner = _onException == null;
_onException += value;
if (subscribeToInner)
_signalTransport.OnException += OnSignalTransportException;
}
}
remove
{
lock (_lock)
{
_onException -= value;
if(_onException==null)
_signalTransport.OnException -= OnSignalTransportException;
}
}
}
private void OnSignalTransportException(IAvaloniaRemoteTransportConnection arg1, Exception ex)
{
_onException?.Invoke(this, ex);
}
#endregion
}
}

472
src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs

@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
{
public class SimpleWebSocketHttpServer : IDisposable
{
private readonly IPAddress _address;
private readonly int _port;
private TcpListener _listener;
public async Task<SimpleWebSocketHttpRequest> AcceptAsync()
{
while (true)
{
var res = await AcceptAsyncImpl();
if (res != null)
return res;
}
}
async Task<SimpleWebSocketHttpRequest> AcceptAsyncImpl()
{
if (_listener == null)
throw new InvalidOperationException("Currently not listening");
var socket = await _listener.AcceptSocketAsync();
var stream = new NetworkStream(socket);
bool error = true;
async Task<string> ReadLineAsync()
{
var readBuffer = new byte[1];
var lineBuffer = new byte[1024];
for (var c = 0; c < 1024; c++)
{
if (await stream.ReadAsync(readBuffer, 0, 1) == 0)
throw new EndOfStreamException();
if (readBuffer[0] == 10)
{
if (c == 0)
return "";
if (lineBuffer[c - 1] == 13)
c--;
if (c == 0)
return "";
return Encoding.UTF8.GetString(lineBuffer, 0, c);
}
lineBuffer[c] = readBuffer[0];
}
throw new InvalidDataException("Header is too large");
}
var headers = new Dictionary<string, string>();
string line = null;
try
{
line = await ReadLineAsync();
var sp = line.Split(' ');
if (sp.Length != 3 || !sp[2].StartsWith("HTTP") || sp[0] != "GET")
return null;
var path = sp[1];
while (true)
{
line = await ReadLineAsync();
if (line == "")
break;
sp = line.Split(new[] {':'}, 2);
headers[sp[0]] = sp[1].TrimStart();
}
error = false;
return new SimpleWebSocketHttpRequest(stream, path, headers);
}
catch
{
error = true;
return null;
}
finally
{
if (error)
stream.Dispose();
}
}
public void Listen()
{
var listener = new TcpListener(_address, _port);
listener.Start();
_listener = listener;
}
public SimpleWebSocketHttpServer(IPAddress address, int port)
{
_address = address;
_port = port;
}
public void Dispose()
{
_listener?.Stop();
_listener = null;
}
}
public class SimpleWebSocketHttpRequest : IDisposable
{
public Dictionary<string, string> Headers { get; }
public string Path { get; }
private NetworkStream _stream;
public bool IsWebsocketRequest { get; }
public IReadOnlyList<string> WebSocketProtocols { get; }
private string _websocketKey;
public SimpleWebSocketHttpRequest(NetworkStream stream, string path, Dictionary<string, string> headers)
{
Path = path;
Headers = headers;
_stream = stream;
if (headers.TryGetValue("Connection", out var h)
&& h.Contains("Upgrade")
&& headers.TryGetValue("Upgrade", out h) &&
h == "websocket"
&& headers.TryGetValue("Sec-WebSocket-Key", out _websocketKey))
{
IsWebsocketRequest = true;
if (headers.TryGetValue("Sec-WebSocket-Protocol", out h))
WebSocketProtocols = h.Split(',').Select(x => x.Trim()).ToArray();
else WebSocketProtocols = new string[0];
}
}
public async Task RespondAsync(int code, byte[] data, string contentType)
{
var headers = Encoding.UTF8.GetBytes($"HTTP/1.1 {code} {(HttpStatusCode)code}\r\nConnection: close\r\nContent-Type: {contentType}\r\nContent-Length: {data.Length}\r\n\r\n");
await _stream.WriteAsync(headers, 0, headers.Length);
await _stream.WriteAsync(data, 0, data.Length);
_stream.Dispose();
_stream = null;
}
public async Task<SimpleWebSocket> AcceptWebSocket(string protocol = null)
{
var handshakeSource = _websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string handshake;
using (var sha1 = SHA1.Create())
handshake = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(handshakeSource)));
var headers =
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "
+ handshake + "\r\n";
if (protocol != null)
headers += protocol + "\r\n";
headers += "\r\n";
var bheaders = Encoding.UTF8.GetBytes(headers);
await _stream.WriteAsync(bheaders, 0, bheaders.Length);
var s = _stream;
_stream = null;
return new SimpleWebSocket(s);
}
public void Dispose() => _stream?.Dispose();
}
public class SimpleWebSocket : IDisposable
{
class AsyncLock
{
private object _syncRoot = new object();
private Queue<TaskCompletionSource<IDisposable>> _queue = new Queue<TaskCompletionSource<IDisposable>>();
private bool _locked;
public Task<IDisposable> LockAsync()
{
lock (_syncRoot)
{
if (!_locked)
{
_locked = true;
return Task.FromResult<IDisposable>(new Lock(this));
}
else
{
var tcs = new TaskCompletionSource<IDisposable>();
_queue.Enqueue(tcs);
return tcs.Task;
}
}
}
private void Unlock()
{
lock (_syncRoot)
{
if (_queue.Count != 0)
_queue.Dequeue().SetResult(new Lock(this));
else
_locked = false;
}
}
class Lock : IDisposable
{
private AsyncLock _parent;
private object _syncRoot = new object();
public Lock(AsyncLock parent)
{
_parent = parent;
}
public void Dispose()
{
lock (_syncRoot)
{
if (_parent == null)
return;
var p = _parent;
_parent = null;
p.Unlock();
}
}
}
}
private Stream _stream;
private AsyncLock _sendLock = new AsyncLock();
private AsyncLock _recvLock = new AsyncLock();
private const int WebsocketInitialHeaderLength = 2;
private const int WebsocketLen16Length = 4;
private const int WebsocketLen64Length = 10;
private const int WebsocketLen16Code = 126;
private const int WebsocketLen64Code = 127;
[StructLayout(LayoutKind.Explicit)]
struct WebSocketHeader
{
[FieldOffset(0)] public byte Mask;
[FieldOffset(1)] public byte Length8;
[FieldOffset(2)] public ushort Length16;
[FieldOffset(2)] public ulong Length64;
}
readonly byte[] _sendHeaderBuffer = new byte[10];
readonly MemoryStream _receiveFrameStream = new MemoryStream();
readonly MemoryStream _receiveMessageStream = new MemoryStream();
private FrameType _currentMessageFrameType;
enum FrameType
{
Continue = 0x0,
Text = 0x1,
Binary = 0x2,
Close = 0x8,
Ping = 0x9,
Pong = 0xA
}
internal SimpleWebSocket(Stream stream)
{
_stream = stream;
}
public void Dispose()
{
_stream?.Dispose();
_stream = null;
}
public Task SendMessage(string text)
{
var data = Encoding.UTF8.GetBytes(text);
return SendMessage(true, data);
}
public Task SendMessage(bool isText, byte[] data) => SendMessage(isText, data, 0, data.Length);
public Task SendMessage(bool isText, byte[] data, int offset, int length)
=> SendFrame(isText ? FrameType.Text : FrameType.Binary, data, offset, length);
async Task SendFrame(FrameType type, byte[] data, int offset, int length)
{
using (var l = await _sendLock.LockAsync())
{
var header = new WebSocketHeader();
int headerLength;
if (data.Length <= 125)
{
headerLength = WebsocketInitialHeaderLength;
header.Length8 = (byte) length;
}
else if (length <= 0xffff)
{
headerLength = WebsocketLen16Length;
header.Length8 = WebsocketLen16Code;
header.Length16 = (ushort) IPAddress.HostToNetworkOrder((short) (ushort) length);
}
else
{
headerLength = WebsocketLen64Length;
header.Length8 = WebsocketLen64Code;
header.Length64 = (ulong) IPAddress.HostToNetworkOrder((long) length);
}
var endOfMessage = true;
header.Mask = (byte) (((endOfMessage ? 1u : 0u) << 7) | ((byte) (type) & 0xf));
unsafe
{
Marshal.Copy(new IntPtr(&header), _sendHeaderBuffer, 0, headerLength);
}
await _stream.WriteAsync(_sendHeaderBuffer, 0, headerLength);
await _stream.WriteAsync(data, offset, length);
}
}
struct Frame
{
public byte[] Data;
public bool EndOfMessage;
public FrameType FrameType;
}
byte[] _recvHeaderBuffer = new byte[8];
byte[] _maskBuffer = new byte[4];
async Task<Frame> ReadFrame()
{
_receiveFrameStream.Position = 0;
_receiveFrameStream.SetLength(0);
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
var masked = (_recvHeaderBuffer[1] & 0x80) != 0;
var len0 = (_recvHeaderBuffer[1] & 0x7F);
var endOfMessage = (_recvHeaderBuffer[0] & 0x80) != 0;
var frameType = (FrameType) (_recvHeaderBuffer[0] & 0xf);
int length;
if (len0 <= 125)
length = len0;
else if (len0 == WebsocketLen16Code)
{
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
length = (ushort) IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_recvHeaderBuffer, 0));
}
else
{
await ReadExact(_stream, _recvHeaderBuffer, 0, 8);
length = (int) (ulong) IPAddress.NetworkToHostOrder((long) BitConverter.ToUInt64(_recvHeaderBuffer, 0));
}
if (masked)
await ReadExact(_stream, _maskBuffer, 0, 4);
await ReadExact(_stream, _receiveFrameStream, length);
var data = _receiveFrameStream.ToArray();
if(masked)
for (var c = 0; c < data.Length; c++)
data[c] = (byte) (data[c] ^ _maskBuffer[c % 4]);
return new Frame
{
Data = data,
EndOfMessage = endOfMessage,
FrameType = frameType
};
}
public async Task<SimpleWebSocketMessage> ReceiveMessage()
{
using (await _recvLock.LockAsync())
{
while (true)
{
var frame = await ReadFrame();
if (frame.FrameType == FrameType.Close)
return null;
if (frame.FrameType == FrameType.Ping)
await SendFrame(FrameType.Pong, frame.Data, 0, frame.Data.Length);
if (frame.FrameType == FrameType.Text || frame.FrameType == FrameType.Binary)
{
var isText = frame.FrameType == FrameType.Text;
if (_receiveMessageStream.Length == 0 && frame.EndOfMessage)
return new SimpleWebSocketMessage
{
IsText = isText,
Data = frame.Data
};
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
_currentMessageFrameType = frame.FrameType;
}
if (frame.FrameType == FrameType.Continue)
{
frame.FrameType = _currentMessageFrameType;
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
if (frame.EndOfMessage)
{
var isText = frame.FrameType == FrameType.Text;
var data = _receiveMessageStream.ToArray();
_receiveMessageStream.Position = 0;
_receiveMessageStream.SetLength(0);
return new SimpleWebSocketMessage
{
IsText = isText,
Data = data
};
}
}
}
}
}
byte[] _readExactBuffer = new byte[4096];
async Task ReadExact(Stream from, MemoryStream to, int length)
{
while (length>0)
{
var toRead = Math.Min(length, _readExactBuffer.Length);
var read = await from.ReadAsync(_readExactBuffer, 0, toRead);
to.Write(_readExactBuffer, 0, read);
if (read <= 0)
throw new EndOfStreamException();
length -= read;
}
}
async Task ReadExact(Stream from, byte[] to, int offset, int length)
{
while (length > 0)
{
var read = await from.ReadAsync(to, offset, length);
if (read <= 0)
throw new EndOfStreamException();
length -= read;
offset += read;
}
}
}
public class SimpleWebSocketMessage
{
public bool IsText { get; set; }
public byte[] Data { get; set; }
public string AsString()
{
return Encoding.UTF8.GetString(Data);
}
}
}

2
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/.gitignore

@ -0,0 +1,2 @@
build
node_modules

8878
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

File diff suppressed because it is too large

41
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json

@ -0,0 +1,41 @@
{
"name": "simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"webpack-ver": "cross-env NODE_ENV=production webpack --version",
"dist": "cross-env NODE_ENV=production webpack --display-error-details",
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details"
},
"author": "",
"license": "ISC",
"devDependencies": {
"awesome-typescript-loader": "^5.0.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.1.6",
"css-loader": "^1.0.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.1",
"source-map-loader": "^0.2.3",
"style-loader": "^0.21.0",
"to-string-loader": "^1.1.5",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^2.9.2",
"url-loader": "^1.0.1",
"webpack": "~4.16.3",
"webpack-cli": "~2.1.3",
"webpack-livereload-plugin": "~2.1.1"
},
"dependencies": {
"@types/react": "^16.3.14",
"@types/react-dom": "^16.0.5",
"mobx": "4.3.0",
"mobx-react": "^5.1.2",
"react": "^16.3.2",
"react-dom": "^16.3.2"
}
}

57
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/FramePresenter.tsx

@ -0,0 +1,57 @@
import {PreviewerFrame, PreviewerServerConnection} from "src/PreviewerServerConnection";
import * as React from "react";
interface PreviewerPresenterProps {
conn: PreviewerServerConnection;
}
export class PreviewerPresenter extends React.Component<PreviewerPresenterProps> {
private canvasRef: React.RefObject<HTMLCanvasElement>;
constructor(props: PreviewerPresenterProps) {
super(props);
this.state = {width: 1, height: 1};
this.canvasRef = React.createRef()
this.componentDidUpdate({
conn: null!
}, this.state);
}
componentDidMount(): void {
this.updateCanvas(this.canvasRef.current, this.props.conn.currentFrame);
}
componentDidUpdate(prevProps: Readonly<PreviewerPresenterProps>, prevState: Readonly<{}>, snapshot?: any): void {
if(prevProps.conn != this.props.conn)
{
if(prevProps.conn)
prevProps.conn.removeFrameListener(this.frameHandler);
if(this.props.conn)
this.props.conn.addFrameListener(this.frameHandler);
}
}
private frameHandler = (frame: PreviewerFrame)=>{
this.updateCanvas(this.canvasRef.current, frame);
};
updateCanvas(canvas: HTMLCanvasElement | null, frame: PreviewerFrame | null) {
if (!canvas)
return;
if (frame == null){
canvas.width = canvas.height = 1;
canvas.getContext('2d')!.clearRect(0,0,1,1);
}
else {
canvas.width = frame.data.width;
canvas.height = frame.data.height;
const ctx = canvas.getContext('2d')!;
ctx.putImageData(frame.data, 0,0);
}
}
render() {
return <canvas ref={this.canvasRef}/>
}
}

78
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/PreviewerServerConnection.ts

@ -0,0 +1,78 @@
export interface PreviewerFrame {
data: ImageData;
dpiX: number;
dpiY: number;
}
export class PreviewerServerConnection {
private nextFrame = {
width: 0,
height: 0,
stride: 0,
dpiX: 0,
dpiY: 0,
sequenceId: "0"
};
public currentFrame: PreviewerFrame | null;
private handlers = new Set<(frame: PreviewerFrame | null) => void>();
private conn: WebSocket;
public addFrameListener(listener: (frame: PreviewerFrame | null) => void) {
this.handlers.add(listener);
if (this.currentFrame)
listener(this.currentFrame);
}
public removeFrameListener(listener: (frame: PreviewerFrame | null) => void) {
this.handlers.delete(listener);
}
constructor(uri: string) {
this.currentFrame = null;
var conn = this.conn = new WebSocket(uri);
conn.binaryType = 'arraybuffer';
const onMessage = this.onMessage;
conn.onmessage = msg => onMessage(msg);
const onClose = () => this.setFrame(null);
conn.onclose = () => onClose();
conn.onerror = (err: Event) => {
onClose();
console.log("Connection error: " + err);
}
}
private onMessage = (msg: MessageEvent) => {
if (typeof msg.data == 'string' || msg.data instanceof String) {
const parts = msg.data.split(':');
if (parts[0] == 'frame') {
this.nextFrame = {
sequenceId: parts[1],
width: parseInt(parts[2]),
height: parseInt(parts[3]),
stride: parseInt(parts[4]),
dpiX: parseInt(parts[5]),
dpiY: parseInt(parts[6])
}
}
} else if (msg.data instanceof ArrayBuffer) {
const arr = new Uint8ClampedArray(msg.data, 0);
const imageData = new ImageData(arr, this.nextFrame.width, this.nextFrame.height);
this.conn.send('frame-received:' + this.nextFrame.sequenceId);
this.setFrame({
data: imageData,
dpiX: this.nextFrame.dpiX,
dpiY: this.nextFrame.dpiY
});
}
};
private setFrame(frame: PreviewerFrame | null) {
this.currentFrame = frame;
this.handlers.forEach(h => h(this.currentFrame));
}
}

14
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Avalonia XAML previewer web edition</title>
</head>
<body>
<div id="app">
<center>Loading...</center>
</div>
<noscript>Javascript is required</noscript>
</body>
</html>

15
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.tsx

@ -0,0 +1,15 @@
import * as React from "react";
import {PreviewerPresenter} from './FramePresenter'
import {PreviewerServerConnection} from "src/PreviewerServerConnection";
import * as ReactDOM from "react-dom";
const loc = window.location;
const conn = new PreviewerServerConnection((loc.protocol === "https:" ? "wss" : "ws") + "://" + loc.host + "/ws");
const App = function(){
return <div style={{width: '100%'}}>
<PreviewerPresenter conn={conn}/>
</div>
};
ReactDOM.render(<App/>, document.getElementById("app"));

35
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json

@ -0,0 +1,35 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"baseUrl": ".",
"experimentalDecorators": true,
"paths": {
"*": ["./node_modules/@types/*", "./node_modules/*"],
"src/*": ["./src/*"]
}
},
"include": ["./src/**/*"],
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
]
}

117
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js

@ -0,0 +1,117 @@
const webpack = require('webpack');
const path = require('path');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const prod = process.env.NODE_ENV == 'production';
class Printer {
apply(compiler) {
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString()));
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString()));
}
}
const config = {
entry: {
bundle: './src/index.tsx'
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/',
filename: '[name].[chunkhash].js'
},
performance: { hints: false },
mode: prod ? "production" : "development",
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
exclude: [
path.resolve(__dirname, 'node_modules/mobx-state-router')
]
},
{
"oneOf": [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'awesome-typescript-loader'
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(jpg|png)$/,
use: {
loader: "url-loader",
options: {
limit: 25000,
},
},
},
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/', // where the fonts will go
}
}]
},
{
loader: require.resolve('file-loader'),
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/],
options: {
name: 'assets/[name].[hash:8].[ext]',
},
}]
},
]
},
devtool: "source-map",
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })],
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'src': path.resolve(__dirname, 'src')
}
},
plugins:
[
new Printer(),
new CleanWebpackPlugin([path.resolve(__dirname, 'build')]),
new MiniCssExtractPlugin({
filename: "[name].[chunkhash]h" +
".css",
chunkFilename: "[id].[chunkhash].css"
}),
new LiveReloadPlugin({appendScriptTag: !prod}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html' //relative to root of the application
}),
new CopyWebpackPlugin([
// relative path from src
//{ from: './src/favicon.ico' },
//{ from: './src/assets' }
]),
new CompressionPlugin({
test: /(\?.*)?$/i
})
]
};
module.exports = config;

53
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -5,6 +5,7 @@ using System.Reflection;
using System.Threading;
using System.Xml;
using Avalonia.Controls;
using Avalonia.DesignerSupport.Remote.HtmlTransport;
using Avalonia.Input;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
@ -24,15 +25,16 @@ namespace Avalonia.DesignerSupport.Remote
{
public string AppPath { get; set; }
public Uri Transport { get; set; }
public Uri HtmlMethodListenUri { get; set; }
public string Method { get; set; } = Methods.AvaloniaRemote;
public string SessionId { get; set; } = Guid.NewGuid().ToString();
}
static class Methods
internal static class Methods
{
public const string AvaloniaRemote = "avalonia-remote";
public const string Win32 = "win32";
public const string Html = "html";
}
static Exception Die(string error)
@ -52,6 +54,19 @@ namespace Avalonia.DesignerSupport.Remote
{
Console.Error.WriteLine("Usage: --transport transport_spec --session-id sid --method method app");
Console.Error.WriteLine();
Console.Error.WriteLine("--transport: transport used for communication with the IDE");
Console.Error.WriteLine(" 'tcp-bson' (e. g. 'tcp-bson://127.0.0.1:30243/') - TCP-based transport with BSON serialization of messages defined in Avalonia.Remote.Protocol");
Console.Error.WriteLine(" 'file' (e. g. 'file://C://my/file.xaml' - pseudo-transport that triggers XAML updates on file changes, useful as a standalone previewer tool, always uses http preview method");
Console.Error.WriteLine();
Console.Error.WriteLine("--session-id: session id to be sent to IDE process");
Console.Error.WriteLine();
Console.Error.WriteLine("--method: the way the XAML is displayed");
Console.Error.WriteLine(" 'avalonia-remote' - binary image is sent via transport connection in FrameMessage");
Console.Error.WriteLine(" 'win32' - XAML is displayed in win32 window (handle could be obtained from UpdateXamlResultMessage), IDE is responsible to use user32!SetParent");
Console.Error.WriteLine(" 'html' - Previewer starts an HTML server and displays XAML previewer as a web page");
Console.Error.WriteLine();
Console.Error.WriteLine("--html-url - endpoint for HTML method to listen on, e. g. http://127.0.0.1:8081");
Console.Error.WriteLine();
Console.Error.WriteLine("Example: --transport tcp-bson://127.0.0.1:30243/ --session-id 123 --method avalonia-remote MyApp.exe");
Console.Error.Flush();
return Die(null);
@ -74,6 +89,8 @@ namespace Avalonia.DesignerSupport.Remote
next = a => rv.Transport = new Uri(a, UriKind.Absolute);
else if (arg == "--method")
next = a => rv.Method = a;
else if (arg == "--html-url")
next = a => rv.HtmlMethodListenUri = new Uri(a, UriKind.Absolute);
else if (arg == "--session-id")
next = a => rv.SessionId = a;
else if (rv.AppPath == null)
@ -89,6 +106,9 @@ namespace Avalonia.DesignerSupport.Remote
{
PrintUsage();
}
if (next != null)
PrintUsage();
return rv;
}
@ -98,27 +118,40 @@ namespace Avalonia.DesignerSupport.Remote
{
return new BsonTcpTransport().Connect(IPAddress.Parse(transport.Host), transport.Port).Result;
}
if (transport.Scheme == "file")
{
return new FileWatcherTransport(transport);
}
PrintUsage();
return null;
}
interface IAppInitializer
{
Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
}
class AppInitializer<T> : IAppInitializer where T : AppBuilderBase<T>, new()
{
public Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport,
public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport,
CommandLineArgs args, object obj)
{
var builder = (AppBuilderBase<T>) obj;
var builder = (AppBuilderBase<T>)obj;
if (args.Method == Methods.AvaloniaRemote)
builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport));
if (args.Method == Methods.Html)
{
transport = new HtmlWebSocketTransport(transport,
args.HtmlMethodListenUri ?? new Uri("http://localhost:5000"));
builder.UseWindowingSubsystem(() =>
PreviewerWindowingPlatform.Initialize(transport));
}
if (args.Method == Methods.Win32)
builder.UseWindowingSubsystem("Avalonia.Win32");
builder.SetupWithoutStarting();
return builder.Instance;
return transport;
}
}
@ -128,6 +161,8 @@ namespace Avalonia.DesignerSupport.Remote
{
var args = ParseCommandLineArgs(cmdline);
var transport = CreateTransport(args.Transport);
if (transport is ITransportWithEnforcedMethod enforcedMethod)
args.Method = enforcedMethod.PreviewerMethod;
var asm = Assembly.LoadFile(System.IO.Path.GetFullPath(args.AppPath));
var entryPoint = asm.EntryPoint;
if (entryPoint == null)
@ -141,12 +176,14 @@ namespace Avalonia.DesignerSupport.Remote
var appBuilder = builderMethod.Invoke(null, null);
Log($"Initializing application in design mode");
var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer<>).MakeGenericType(appBuilder.GetType()));
var app = initializer.GetConfiguredApp(transport, args, appBuilder);
transport = initializer.ConfigureApp(transport, args, appBuilder);
s_transport = transport;
transport.OnMessage += OnTransportMessage;
transport.OnException += (t, e) => Die(e.ToString());
transport.Start();
Log("Sending StartDesignerSessionMessage");
transport.Send(new StartDesignerSessionMessage {SessionId = args.SessionId});
Dispatcher.UIThread.MainLoop(CancellationToken.None);
}

4
src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs

@ -34,7 +34,7 @@ namespace Avalonia.Dialogs
return;
}
var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control);
var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
if (e.ClickCount == 2 || isQuickLink)
{
if (model.ItemType == ManagedFileChooserItemType.File)
@ -81,7 +81,7 @@ namespace Avalonia.Dialogs
if (indexOfPreselected > 1)
{
_filesView.ScrollIntoView(model.Items[indexOfPreselected - 1]);
_filesView.ScrollIntoView(indexOfPreselected - 1);
}
}
}

14
src/Avalonia.Dialogs/ManagedFileChooserViewModel.cs

@ -14,6 +14,7 @@ namespace Avalonia.Dialogs
{
internal class ManagedFileChooserViewModel : InternalViewModelBase
{
private readonly ManagedFileDialogOptions _options;
public event Action CancelRequested;
public event Action<string[]> CompleteRequested;
@ -103,8 +104,9 @@ namespace Avalonia.Dialogs
QuickLinks.AddRange(quickSources.GetAllItems().Select(i => new ManagedFileChooserItemViewModel(i)));
}
public ManagedFileChooserViewModel(FileSystemDialog dialog)
public ManagedFileChooserViewModel(FileSystemDialog dialog, ManagedFileDialogOptions options)
{
_options = options;
_disposables = new CompositeDisposable();
var quickSources = AvaloniaLocator.Current
@ -134,7 +136,7 @@ namespace Avalonia.Dialogs
: dialog is OpenFolderDialog ? "Select directory"
: throw new ArgumentException(nameof(dialog)));
var directory = dialog.InitialDirectory;
var directory = dialog.Directory;
if (directory == null || !Directory.Exists(directory))
{
@ -202,10 +204,12 @@ namespace Avalonia.Dialogs
}
else
{
var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder).ToList();
foreach (var item in invalidItems)
if (!_options.AllowDirectorySelection)
{
SelectedItems.Remove(item);
var invalidItems = SelectedItems.Where(i => i.ItemType == ManagedFileChooserItemType.Folder)
.ToList();
foreach (var item in invalidItems)
SelectedItems.Remove(item);
}
if (!_selectingDirectory)

20
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -9,13 +9,15 @@ namespace Avalonia.Dialogs
{
private class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
{
async Task<string[]> Show(SystemDialog d, Window parent)
async Task<string[]> Show(SystemDialog d, Window parent, ManagedFileDialogOptions options = null)
{
var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
var model = new ManagedFileChooserViewModel((FileSystemDialog)d,
options ?? new ManagedFileDialogOptions());
var dialog = new T
{
Content = new ManagedFileChooser(),
Title = d.Title,
DataContext = model
};
@ -44,6 +46,11 @@ namespace Avalonia.Dialogs
{
return (await Show(dialog, parent))?.FirstOrDefault();
}
public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent, ManagedFileDialogOptions options)
{
return await Show(dialog, parent, options);
}
}
public static TAppBuilder UseManagedSystemDialogs<TAppBuilder>(this TAppBuilder builder)
@ -61,5 +68,14 @@ namespace Avalonia.Dialogs
AvaloniaLocator.CurrentMutable.Bind<ISystemDialogImpl>().ToSingleton<ManagedSystemDialogImpl<TWindow>>());
return builder;
}
public static Task<string[]> ShowManagedAsync(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) => ShowManagedAsync<Window>(dialog, parent, options);
public static Task<string[]> ShowManagedAsync<TWindow>(this OpenFileDialog dialog, Window parent,
ManagedFileDialogOptions options = null) where TWindow : Window, new()
{
return new ManagedSystemDialogImpl<TWindow>().ShowFileDialogAsync(dialog, parent, options);
}
}
}

7
src/Avalonia.Dialogs/ManagedFileDialogOptions.cs

@ -0,0 +1,7 @@
namespace Avalonia.Dialogs
{
public class ManagedFileDialogOptions
{
public bool AllowDirectorySelection { get; set; }
}
}

4
src/Avalonia.Input/DragEventArgs.cs

@ -52,8 +52,10 @@ namespace Avalonia.Input
Data = data;
_target = target;
_targetLocation = targetLocation;
Modifiers = (InputModifiers)keyModifiers;
KeyModifiers = keyModifiers;
#pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)keyModifiers;
#pragma warning restore CS0618 // Type or member is obsolete
}
}

3
src/Avalonia.Input/FocusManager.cs

@ -186,8 +186,9 @@ namespace Avalonia.Input
private static void OnPreviewPointerPressed(object sender, RoutedEventArgs e)
{
var ev = (PointerPressedEventArgs)e;
var visual = (IVisual)sender;
if (sender == e.Source && ev.MouseButton == MouseButton.Left)
if (sender == e.Source && ev.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{
IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement;

4
src/Avalonia.Input/Gestures.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
@ -71,12 +72,13 @@ namespace Avalonia.Input
if (ev.Route == RoutingStrategies.Bubble)
{
var e = (PointerPressedEventArgs)ev;
var visual = (IVisual)ev.Source;
if (e.ClickCount <= 1)
{
s_lastPress = new WeakReference<IInteractive>(e.Source);
}
else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton == MouseButton.Left)
else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
{
if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
{

6
src/Avalonia.Input/PointerEventArgs.cs

@ -128,10 +128,10 @@ namespace Avalonia.Input
_obsoleteClickCount = obsoleteClickCount;
}
[Obsolete("Use DoubleTapped or DoubleRightTapped event instead")]
[Obsolete("Use DoubleTapped event or Gestures.DoubleRightTapped attached event")]
public int ClickCount => _obsoleteClickCount;
[Obsolete("Use PointerUpdateKind")]
[Obsolete("Use PointerPressedEventArgs.GetCurrentPoint(this).Properties")]
public MouseButton MouseButton => Properties.PointerUpdateKind.GetMouseButton();
}
@ -153,7 +153,7 @@ namespace Avalonia.Input
/// </summary>
public MouseButton InitialPressMouseButton { get; }
[Obsolete("Either use GetCurrentPoint(this).Properties.PointerUpdateKind or InitialPressMouseButton, see https://github.com/AvaloniaUI/Avalonia/wiki/Pointer-events-in-0.9 for more details", true)]
[Obsolete("Use InitialPressMouseButton")]
public MouseButton MouseButton => InitialPressMouseButton;
}

4
src/Avalonia.Input/Raw/RawDragEvent.cs

@ -20,8 +20,10 @@ namespace Avalonia.Input.Raw
Location = location;
Data = data;
Effects = effects;
Modifiers = (InputModifiers)modifiers;
KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers);
#pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

6
src/Avalonia.Native/SystemDialogs.cs

@ -28,7 +28,7 @@ namespace Avalonia.Native
_native.OpenFileDialog(nativeParent,
events, ofd.AllowMultiple,
ofd.Title ?? "",
ofd.InitialDirectory ?? "",
ofd.Directory ?? "",
ofd.InitialFileName ?? "",
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions)));
}
@ -37,7 +37,7 @@ namespace Avalonia.Native
_native.SaveFileDialog(nativeParent,
events,
dialog.Title ?? "",
dialog.InitialDirectory ?? "",
dialog.Directory ?? "",
dialog.InitialFileName ?? "",
string.Join(";", dialog.Filters.SelectMany(f => f.Extensions)));
}
@ -51,7 +51,7 @@ namespace Avalonia.Native
var nativeParent = GetNativeWindow(parent);
_native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? "");
_native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.Directory ?? "");
return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); });
}

2
src/Avalonia.Native/WindowImpl.cs

@ -68,7 +68,7 @@ namespace Avalonia.Native
public void SetSystemDecorations(Controls.SystemDecorations enabled)
{
_native.HasDecorations = (Interop.SystemDecorations)enabled;
_native.Decorations = (Interop.SystemDecorations)enabled;
}
public void SetTitleBarColor (Avalonia.Media.Color color)

4
src/Avalonia.Remote.Protocol/BsonStreamTransport.cs

@ -144,5 +144,9 @@ namespace Avalonia.Remote.Protocol
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage;
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
public void Start()
{
}
}
}

1
src/Avalonia.Remote.Protocol/ITransport.cs

@ -8,5 +8,6 @@ namespace Avalonia.Remote.Protocol
Task Send(object data);
event Action<IAvaloniaRemoteTransportConnection, object> OnMessage;
event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
void Start();
}
}

2
src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs

@ -98,5 +98,7 @@ namespace Avalonia.Remote.Protocol
add => _onException.Add(value);
remove => _onException.Remove(value);
}
public void Start() => _conn.Start();
}
}

9
src/Avalonia.Remote.Protocol/TransportMessages.cs

@ -0,0 +1,9 @@
namespace Avalonia.Remote.Protocol
{
[AvaloniaRemoteMessageGuid("53778004-78fa-4381-8ec3-176a6f2328b6")]
public class HtmlTransportStartedMessage
{
public string Uri { get; set; }
}
}

2
src/Avalonia.Remote.Protocol/ViewportMessages.cs

@ -60,6 +60,8 @@
public int Width { get; set; }
public int Height { get; set; }
public int Stride { get; set; }
public double DpiX { get; set; }
public double DpiY { get; set; }
}
}

148
src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs

@ -1,11 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.LogicalTree
{
/// <summary>
/// Provides extension methods for working with the logical tree.
/// </summary>
public static class LogicalExtensions
{
/// <summary>
/// Enumerates the ancestors of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical's ancestors.</returns>
public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical)
{
Contract.Requires<ArgumentNullException>(logical != null);
@ -19,6 +26,11 @@ namespace Avalonia.LogicalTree
}
}
/// <summary>
/// Enumerates an <see cref="ILogical"/> and its ancestors in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical and its ancestors.</returns>
public static IEnumerable<ILogical> GetSelfAndLogicalAncestors(this ILogical logical)
{
yield return logical;
@ -29,11 +41,50 @@ namespace Avalonia.LogicalTree
}
}
/// <summary>
/// Finds first ancestor of given type.
/// </summary>
/// <typeparam name="T">Ancestor type.</typeparam>
/// <param name="logical">The logical.</param>
/// <param name="includeSelf">If given logical should be included in search.</param>
/// <returns>First ancestor of given type.</returns>
public static T FindLogicalAncestorOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
{
if (logical is null)
{
return null;
}
ILogical parent = includeSelf ? logical : logical.LogicalParent;
while (parent != null)
{
if (parent is T result)
{
return result;
}
parent = parent.LogicalParent;
}
return null;
}
/// <summary>
/// Enumerates the children of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical children.</returns>
public static IEnumerable<ILogical> GetLogicalChildren(this ILogical logical)
{
return logical.LogicalChildren;
}
/// <summary>
/// Enumerates the descendants of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical's ancestors.</returns>
public static IEnumerable<ILogical> GetLogicalDescendants(this ILogical logical)
{
foreach (ILogical child in logical.LogicalChildren)
@ -47,6 +98,11 @@ namespace Avalonia.LogicalTree
}
}
/// <summary>
/// Enumerates an <see cref="ILogical"/> and its descendants in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical and its ancestors.</returns>
public static IEnumerable<ILogical> GetSelfAndLogicalDescendants(this ILogical logical)
{
yield return logical;
@ -57,16 +113,56 @@ namespace Avalonia.LogicalTree
}
}
/// <summary>
/// Finds first descendant of given type.
/// </summary>
/// <typeparam name="T">Descendant type.</typeparam>
/// <param name="logical">The logical.</param>
/// <param name="includeSelf">If given logical should be included in search.</param>
/// <returns>First descendant of given type.</returns>
public static T FindLogicalDescendantOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
{
if (logical is null)
{
return null;
}
if (includeSelf && logical is T result)
{
return result;
}
return FindDescendantOfTypeCore<T>(logical);
}
/// <summary>
/// Gets the logical parent of an <see cref="ILogical"/>.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The parent, or null if the logical is unparented.</returns>
public static ILogical GetLogicalParent(this ILogical logical)
{
return logical.LogicalParent;
}
/// <summary>
/// Gets the logical parent of an <see cref="ILogical"/>.
/// </summary>
/// <typeparam name="T">The type of the logical parent.</typeparam>
/// <param name="logical">The logical.</param>
/// <returns>
/// The parent, or null if the logical is unparented or its parent is not of type <typeparamref name="T"/>.
/// </returns>
public static T GetLogicalParent<T>(this ILogical logical) where T : class
{
return logical.LogicalParent as T;
}
/// <summary>
/// Enumerates the siblings of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical siblings.</returns>
public static IEnumerable<ILogical> GetLogicalSiblings(this ILogical logical)
{
ILogical parent = logical.LogicalParent;
@ -80,9 +176,55 @@ namespace Avalonia.LogicalTree
}
}
public static bool IsLogicalParentOf(this ILogical logical, ILogical target)
/// <summary>
/// Tests whether an <see cref="ILogical"/> is an ancestor of another logical.
/// </summary>
/// <param name="logical">The logical.</param>
/// <param name="target">The potential descendant.</param>
/// <returns>
/// True if <paramref name="logical"/> is an ancestor of <paramref name="target"/>;
/// otherwise false.
/// </returns>
public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target)
{
return target.GetLogicalAncestors().Any(x => x == logical);
ILogical current = target?.LogicalParent;
while (current != null)
{
if (current == logical)
{
return true;
}
current = current.LogicalParent;
}
return false;
}
private static T FindDescendantOfTypeCore<T>(ILogical logical) where T : class
{
var logicalChildren = logical.LogicalChildren;
var logicalChildrenCount = logicalChildren.Count;
for (var i = 0; i < logicalChildrenCount; i++)
{
ILogical child = logicalChildren[i];
if (child is T result)
{
return result;
}
var childResult = FindDescendantOfTypeCore<T>(child);
if (!(childResult is null))
{
return childResult;
}
}
return null;
}
}
}

23
src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs

@ -0,0 +1,23 @@
using Avalonia.Media;
namespace Avalonia.Animation.Animators
{
public class BoxShadowAnimator : Animator<BoxShadow>
{
static ColorAnimator s_colorAnimator = new ColorAnimator();
static DoubleAnimator s_doubleAnimator = new DoubleAnimator();
static BoolAnimator s_boolAnimator = new BoolAnimator();
public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue)
{
return new BoxShadow
{
OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur),
Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread),
Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color),
IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset)
};
}
}
}

40
src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs

@ -0,0 +1,40 @@
using Avalonia.Media;
namespace Avalonia.Animation.Animators
{
public class BoxShadowsAnimator : Animator<BoxShadows>
{
private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator();
public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue)
{
int cnt = progress >= 1d ? newValue.Count : oldValue.Count;
if (cnt == 0)
return new BoxShadows();
BoxShadow first;
if (oldValue.Count > 0 && newValue.Count > 0)
first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]);
else if (oldValue.Count > 0)
first = oldValue[0];
else
first = newValue[0];
if (cnt == 1)
return new BoxShadows(first);
var rest = new BoxShadow[cnt - 1];
for (var c = 0; c < rest.Length; c++)
{
var idx = c + 1;
if (oldValue.Count > idx && newValue.Count > idx)
rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]);
else if (oldValue.Count > idx)
rest[c] = oldValue[idx];
else
rest[c] = newValue[idx];
}
return new BoxShadows(first, rest);
}
}
}

133
src/Avalonia.Visuals/Media/BoxShadow.cs

@ -0,0 +1,133 @@
using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public struct BoxShadow
{
public double OffsetX { get; set; }
public double OffsetY { get; set; }
public double Blur { get; set; }
public double Spread { get; set; }
public Color Color { get; set; }
public bool IsInset { get; set; }
static BoxShadow()
{
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop =>
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType));
}
public bool Equals(in BoxShadow other)
{
return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color);
}
public override bool Equals(object obj)
{
return obj is BoxShadow other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = OffsetX.GetHashCode();
hashCode = (hashCode * 397) ^ OffsetY.GetHashCode();
hashCode = (hashCode * 397) ^ Blur.GetHashCode();
hashCode = (hashCode * 397) ^ Spread.GetHashCode();
hashCode = (hashCode * 397) ^ Color.GetHashCode();
return hashCode;
}
}
public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
private readonly static char[] s_Separator = new char[] { ' ', '\t' };
struct ArrayReader
{
private int _index;
private string[] _arr;
public ArrayReader(string[] arr)
{
_arr = arr;
_index = 0;
}
public bool TryReadString(out string s)
{
s = null;
if (_index >= _arr.Length)
return false;
s = _arr[_index];
_index++;
return true;
}
public string ReadString()
{
if(!TryReadString(out var rv))
throw new FormatException();
return rv;
}
}
public static unsafe BoxShadow Parse(string s)
{
if(s == null)
throw new ArgumentNullException();
if (s.Length == 0)
throw new FormatException();
var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries);
if (p.Length == 1 && p[0] == "none")
return default;
if (p.Length < 3 || p.Length > 6)
throw new FormatException();
bool inset = false;
var tokenizer = new ArrayReader(p);
string firstToken = tokenizer.ReadString();
if (firstToken == "inset")
{
inset = true;
firstToken = tokenizer.ReadString();
}
var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture);
var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture);
double blur = 0;
double spread = 0;
tokenizer.TryReadString(out var token3);
tokenizer.TryReadString(out var token4);
tokenizer.TryReadString(out var token5);
if (token4 != null)
blur = double.Parse(token3, CultureInfo.InvariantCulture);
if (token5 != null)
spread = double.Parse(token4, CultureInfo.InvariantCulture);
var color = Color.Parse(token5 ?? token4 ?? token3);
return new BoxShadow
{
IsInset = inset,
OffsetX = offsetX,
OffsetY = offsetY,
Blur = blur,
Spread = spread,
Color = color
};
}
public Rect TransformBounds(in Rect rect)
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
}
}

137
src/Avalonia.Visuals/Media/BoxShadows.cs

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Animation.Animators;
namespace Avalonia.Media
{
public struct BoxShadows
{
private readonly BoxShadow _first;
private readonly BoxShadow[] _list;
public int Count { get; }
static BoxShadows()
{
Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop =>
typeof(BoxShadows).IsAssignableFrom(prop.PropertyType));
}
public BoxShadows(BoxShadow shadow)
{
_first = shadow;
_list = null;
Count = 1;
}
public BoxShadows(BoxShadow first, BoxShadow[] rest)
{
_first = first;
_list = rest;
Count = 1 + (rest?.Length ?? 0);
}
public BoxShadow this[int c]
{
get
{
if (c< 0 || c >= Count)
throw new IndexOutOfRangeException();
if (c == 0)
return _first;
return _list[c - 1];
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public struct BoxShadowsEnumerator
{
private int _index;
private BoxShadows _shadows;
public BoxShadowsEnumerator(BoxShadows shadows)
{
_shadows = shadows;
_index = -1;
}
public BoxShadow Current => _shadows[_index];
public bool MoveNext()
{
_index++;
return _index < _shadows.Count;
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this);
private static readonly char[] s_Separators = new[] { ',' };
public static BoxShadows Parse(string s)
{
var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries);
if (sp.Length == 0
|| (sp.Length == 1 &&
(string.IsNullOrWhiteSpace(sp[0])
|| sp[0] == "none")))
return new BoxShadows();
var first = BoxShadow.Parse(sp[0]);
if (sp.Length == 1)
return new BoxShadows(first);
var rest = new BoxShadow[sp.Length - 1];
for (var c = 0; c < rest.Length; c++)
rest[c] = BoxShadow.Parse(sp[c + 1]);
return new BoxShadows(first, rest);
}
public Rect TransformBounds(in Rect rect)
{
var final = rect;
foreach (var shadow in this)
final = final.Union(shadow.TransformBounds(rect));
return final;
}
public bool HasInsetShadows
{
get
{
foreach(var boxShadow in this)
if (!boxShadow.IsEmpty && boxShadow.IsInset)
return true;
return false;
}
}
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
public bool Equals(BoxShadows other)
{
if (other.Count != Count)
return false;
for(var c=0; c<Count ; c++)
if (!this[c].Equals(other[c]))
return false;
return true;
}
public override bool Equals(object obj)
{
return obj is BoxShadows other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = 0;
foreach (var s in this)
hashCode = (hashCode * 397) ^ s.GetHashCode();
return hashCode;
}
}
}
}

44
src/Avalonia.Visuals/Media/Color.cs

@ -118,6 +118,50 @@ namespace Avalonia.Media
throw new FormatException($"Invalid color string: '{s}'.");
}
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <param name="color">The parsed color</param>
/// <returns>The status of the operation.</returns>
public static bool TryParse(ReadOnlySpan<char> s, out Color color)
{
color = default;
if (s == null)
return false;
if (s.Length == 0)
return false;
if (s[0] == '#')
{
var or = 0u;
if (s.Length == 7)
{
or = 0xff000000;
}
else if (s.Length != 9)
{
return false;
}
if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
return false;
color = FromUInt32(parsed| or);
return true;
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
return false;
}
/// <summary>
/// Returns the string representation of the color.
/// </summary>

6
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -141,11 +141,13 @@ namespace Avalonia.Media
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <param name="boxShadow">Box shadow effect parameters</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0)
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0,
BoxShadow boxShadow = default)
{
if (brush == null && !PenIsVisible(pen))
{
@ -162,7 +164,7 @@ namespace Avalonia.Media
radiusY = Math.Min(radiusY, rect.Height / 2);
}
PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY);
PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow);
}
/// <summary>

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

Loading…
Cancel
Save