Browse Source

Merge remote-tracking branch 'origin/master' into repro-items-repeater-issue

repro-items-repeater-issue
Dan Walmsley 6 years ago
parent
commit
db0c9b6c03
  1. 2
      Documentation/build.md
  2. 4
      build/SkiaSharp.props
  3. 49
      native/Avalonia.Native/inc/avalonia-native.h
  4. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  5. 3
      native/Avalonia.Native/src/OSX/AvnString.h
  6. 49
      native/Avalonia.Native/src/OSX/AvnString.mm
  7. 90
      native/Avalonia.Native/src/OSX/clipboard.mm
  8. 8
      native/Avalonia.Native/src/OSX/common.h
  9. 89
      native/Avalonia.Native/src/OSX/dnd.mm
  10. 19
      native/Avalonia.Native/src/OSX/main.mm
  11. 2
      native/Avalonia.Native/src/OSX/window.h
  12. 108
      native/Avalonia.Native/src/OSX/window.mm
  13. 14
      nukebuild/Build.cs
  14. 5
      packages/Avalonia/AvaloniaBuildTasks.targets
  15. 4
      samples/BindingDemo/MainWindow.xaml
  16. 5
      samples/BindingDemo/ViewModels/MainWindowViewModel.cs
  17. 2
      samples/ControlCatalog.NetCore/Program.cs
  18. 4
      samples/ControlCatalog/ControlCatalog.csproj
  19. 1
      samples/ControlCatalog/MainView.xaml
  20. 1
      samples/ControlCatalog/Pages/DialogsPage.xaml
  21. 14
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  22. 12
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  23. 92
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  24. 2
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  25. 29
      samples/ControlCatalog/Pages/OpenGlPage.xaml
  26. 401
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  27. 3
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  28. 99
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  29. BIN
      samples/ControlCatalog/Pages/teapot.bin
  30. 115
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  31. 53
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  32. 3
      samples/RenderDemo/MainWindow.xaml
  33. 28
      samples/RenderDemo/Pages/AnimationsPage.xaml
  34. 9
      samples/RenderDemo/Pages/LineBoundsPage.xaml
  35. 19
      samples/RenderDemo/Pages/LineBoundsPage.xaml.cs
  36. 3
      samples/RenderDemo/RenderDemo.csproj
  37. 2
      samples/VirtualizationDemo/MainWindow.xaml
  38. 14
      samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs
  39. 10
      scripts/ReplaceNugetCache.ps1
  40. 10
      scripts/ReplaceNugetCacheRelease.ps1
  41. 50
      src/Avalonia.Base/Utilities/DisposableLock.cs
  42. 11
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  43. 9
      src/Avalonia.Build.Tasks/Extensions.cs
  44. 31
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  45. 4
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  46. 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  47. 6
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  48. 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  49. 6
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  50. 6
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  51. 6
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  52. 8
      src/Avalonia.Controls/Application.cs
  53. 6
      src/Avalonia.Controls/AutoCompleteBox.cs
  54. 25
      src/Avalonia.Controls/Border.cs
  55. 2
      src/Avalonia.Controls/Button.cs
  56. 2
      src/Avalonia.Controls/ButtonSpinner.cs
  57. 4
      src/Avalonia.Controls/Calendar/Calendar.cs
  58. 6
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  59. 6
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  60. 4
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  61. 6
      src/Avalonia.Controls/Calendar/DatePicker.cs
  62. 8
      src/Avalonia.Controls/ComboBox.cs
  63. 39
      src/Avalonia.Controls/ContextMenu.cs
  64. 20
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  65. 249
      src/Avalonia.Controls/ISelectionModel.cs
  66. 3
      src/Avalonia.Controls/Image.cs
  67. 180
      src/Avalonia.Controls/IndexPath.cs
  68. 232
      src/Avalonia.Controls/IndexRange.cs
  69. 22
      src/Avalonia.Controls/ListBox.cs
  70. 4
      src/Avalonia.Controls/MenuItem.cs
  71. 4
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  72. 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  73. 4
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  74. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  75. 2
      src/Avalonia.Controls/Presenters/IItemsPresenter.cs
  76. 4
      src/Avalonia.Controls/Presenters/ItemVirtualizer.cs
  77. 15
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  78. 15
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  79. 4
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  80. 2
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  81. 4
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  82. 839
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  83. 11
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  84. 2
      src/Avalonia.Controls/ProgressBar.cs
  85. 4
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  86. 2
      src/Avalonia.Controls/RepeatButton.cs
  87. 28
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  88. 2
      src/Avalonia.Controls/Repeater/ItemsSourceView.cs
  89. 36
      src/Avalonia.Controls/Repeater/ViewManager.cs
  90. 49
      src/Avalonia.Controls/SelectedItems.cs
  91. 864
      src/Avalonia.Controls/SelectionModel.cs
  92. 170
      src/Avalonia.Controls/SelectionModelChangeSet.cs
  93. 83
      src/Avalonia.Controls/SelectionModelChildrenRequestedEventArgs.cs
  94. 47
      src/Avalonia.Controls/SelectionModelSelectionChangedEventArgs.cs
  95. 950
      src/Avalonia.Controls/SelectionNode.cs
  96. 110
      src/Avalonia.Controls/SelectionNodeOperation.cs
  97. 2
      src/Avalonia.Controls/Slider.cs
  98. 6
      src/Avalonia.Controls/TabControl.cs
  99. 2
      src/Avalonia.Controls/TextBox.cs
  100. 671
      src/Avalonia.Controls/TreeView.cs

2
Documentation/build.md

@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS:
```
brew install castxml
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
```
On Debian based Linux (Debian, Ubuntu, Mint, etc):

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>

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

@ -22,6 +22,9 @@ struct IAvnGlSurfaceRenderTarget;
struct IAvnGlSurfaceRenderingSession;
struct IAvnMenu;
struct IAvnMenuItem;
struct IAvnStringArray;
struct IAvnDndResultCallback;
struct IAvnGCHandleDeallocatorCallback;
struct IAvnMenuEvents;
enum SystemDecorations {
@ -130,6 +133,22 @@ enum AvnInputModifiers
XButton2MouseButton = 256
};
enum class AvnDragDropEffects
{
None = 0,
Copy = 1,
Move = 2,
Link = 4,
};
enum class AvnDragEventType
{
Enter,
Over,
Leave,
Drop
};
enum AvnWindowState
{
Normal,
@ -188,7 +207,7 @@ enum AvnMenuItemToggleType
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{
public:
virtual HRESULT Initialize() = 0;
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0;
virtual IAvnMacOptions* GetMacOptions() = 0;
virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0;
virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0;
@ -196,6 +215,7 @@ public:
virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0;
virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0;
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
@ -236,6 +256,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0;
};
AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
@ -271,6 +293,9 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0;
virtual void ScalingChanged(double scaling) = 0;
virtual void RunRenderPriorityJobs() = 0;
virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers, AvnDragDropEffects effects,
IAvnClipboard* clipboard, void* dataObjectHandle) = 0;
};
@ -354,8 +379,10 @@ AVNCOM(IAvnScreens, 0e) : IUnknown
AVNCOM(IAvnClipboard, 0f) : IUnknown
{
virtual HRESULT GetText (IAvnString**ppv) = 0;
virtual HRESULT SetText (void* utf8Text) = 0;
virtual HRESULT GetText (char* type, IAvnString**ppv) = 0;
virtual HRESULT SetText (char* type, void* utf8Text) = 0;
virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0;
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0;
virtual HRESULT Clear() = 0;
};
@ -428,4 +455,20 @@ AVNCOM(IAvnMenuEvents, 1A) : IUnknown
virtual void NeedsUpdate () = 0;
};
AVNCOM(IAvnStringArray, 20) : IUnknown
{
virtual unsigned int GetCount() = 0;
virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0;
};
AVNCOM(IAvnDndResultCallback, 21) : IUnknown
{
virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0;
};
AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown
{
virtual void FreeGCHandle(void* handle) = 0;
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -12,6 +12,7 @@
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
@ -33,6 +34,7 @@
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = "<group>"; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = "<group>"; };
37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
@ -92,6 +94,7 @@
5BF943652167AD1D009CAE35 /* cursor.h */,
5B21A981216530F500CEE36E /* cursor.mm */,
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
1A465D0F246AB61600C5858B /* dnd.mm */,
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */,
AB661C212148288600291242 /* common.h */,
379860FE214DA0C000CD0246 /* KeyTransform.h */,
@ -196,6 +199,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,
1A465D10246AB61600C5858B /* dnd.mm in Sources */,
AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
AB661C202148286E00291242 /* window.mm in Sources */,

3
native/Avalonia.Native/src/OSX/AvnString.h

@ -10,5 +10,6 @@
#define AvnString_h
extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
#endif /* AvnString_h */

49
native/Avalonia.Native/src/OSX/AvnString.mm

@ -7,6 +7,7 @@
//
#include "common.h"
#include <vector>
class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
{
@ -61,7 +62,55 @@ public:
}
};
class AvnStringArrayImpl : public virtual ComSingleObject<IAvnStringArray, &IID_IAvnStringArray>
{
private:
std::vector<ComPtr<IAvnString>> _list;
public:
FORWARD_IUNKNOWN()
AvnStringArrayImpl(NSArray<NSString*>* array)
{
for(int c = 0; c < [array count]; c++)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl([array objectAtIndex:c]);
_list.push_back(s);
}
}
AvnStringArrayImpl(NSString* string)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl(string);
_list.push_back(s);
}
virtual unsigned int GetCount() override
{
return (unsigned int)_list.size();
}
virtual HRESULT Get(unsigned int index, IAvnString**ppv) override
{
if(_list.size() <= index)
return E_INVALIDARG;
*ppv = _list[index].getRetainedReference();
return S_OK;
}
};
IAvnString* CreateAvnString(NSString* string)
{
return new AvnStringImpl(string);
}
IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
{
return new AvnStringArrayImpl(array);
}
IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);
}

90
native/Avalonia.Native/src/OSX/clipboard.mm

@ -3,16 +3,27 @@
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
{
private:
NSPasteboard* _pb;
NSPasteboardItem* _item;
public:
FORWARD_IUNKNOWN()
Clipboard()
Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item)
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard stringForType:NSPasteboardTypeString];
if(pasteboard == nil && item == nil)
pasteboard = [NSPasteboard generalPasteboard];
_pb = pasteboard;
_item = item;
}
virtual HRESULT GetText (IAvnString**ppv) override
NSPasteboardItem* TryGetItem()
{
return _item;
}
virtual HRESULT GetText (char* type, IAvnString**ppv) override
{
@autoreleasepool
{
@ -20,20 +31,53 @@ public:
{
return E_POINTER;
}
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString];
*ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]);
*ppv = CreateAvnString(string);
return S_OK;
}
}
virtual HRESULT SetText (void* utf8String) override
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override
{
@autoreleasepool
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString];
*ppv= nil;
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString];
if(data == nil)
return S_OK;
if([data isKindOfClass: [NSString class]])
{
*ppv = CreateAvnStringArray((NSString*) data);
return S_OK;
}
NSArray* arr = (NSArray*)data;
for(int c = 0; c < [arr count]; c++)
if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])
return E_INVALIDARG;
*ppv = CreateAvnStringArray(arr);
return S_OK;
}
}
virtual HRESULT SetText (char* type, void* utf8String) override
{
Clear();
@autoreleasepool
{
auto string = [NSString stringWithUTF8String:(const char*)utf8String];
auto typeString = [NSString stringWithUTF8String:(const char*)type];
if(_item == nil)
[_pb setString: string forType: typeString];
else
[_item setString: string forType:typeString];
}
return S_OK;
@ -43,16 +87,34 @@ public:
{
@autoreleasepool
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setString:@"" forType:NSPasteboardTypeString];
if(_item != nil)
_item = [NSPasteboardItem new];
else
{
[_pb clearContents];
[_pb setString:@"" forType:NSPasteboardTypeString];
}
}
return S_OK;
}
virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
{
*ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]);
return S_OK;
}
};
extern IAvnClipboard* CreateClipboard()
extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item)
{
return new Clipboard(pb, item);
}
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb)
{
return new Clipboard();
auto clipboard = dynamic_cast<Clipboard*>(cb);
if(clipboard == nil)
return nil;
return clipboard->TryGetItem();
}

8
native/Avalonia.Native/src/OSX/common.h

@ -8,11 +8,17 @@
#include <pthread.h>
extern IAvnPlatformThreadingInterface* CreatePlatformThreading();
extern void FreeAvnGCHandle(void* handle);
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl);
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl);
extern IAvnSystemDialogs* CreateSystemDialogs();
extern IAvnScreens* CreateScreens();
extern IAvnClipboard* CreateClipboard();
extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*);
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*);
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle);
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info);
extern NSString* GetAvnCustomDataType();
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop);
extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);

89
native/Avalonia.Native/src/OSX/dnd.mm

@ -0,0 +1,89 @@
#include "common.h"
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop)
{
int effects = 0;
if((nsop & NSDragOperationCopy) != 0)
effects |= (int)AvnDragDropEffects::Copy;
if((nsop & NSDragOperationMove) != 0)
effects |= (int)AvnDragDropEffects::Move;
if((nsop & NSDragOperationLink) != 0)
effects |= (int)AvnDragDropEffects::Link;
return (AvnDragDropEffects)effects;
};
extern NSString* GetAvnCustomDataType()
{
char buffer[256];
sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid());
return [NSString stringWithUTF8String:buffer];
}
@interface AvnDndSource : NSObject<NSDraggingSource>
@end
@implementation AvnDndSource
{
NSDragOperation _operation;
ComPtr<IAvnDndResultCallback> _cb;
void* _sourceHandle;
};
- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
return NSDragOperationCopy;
}
- (AvnDndSource*) initWithOperation: (NSDragOperation)operation
andCallback: (IAvnDndResultCallback*) cb
andSourceHandle: (void*) handle
{
self = [super init];
_operation = operation;
_cb = cb;
_sourceHandle = handle;
return self;
}
- (void)draggingSession:(NSDraggingSession *)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation
{
if(_cb != nil)
{
auto cb = _cb;
_cb = nil;
cb->OnDragAndDropComplete(ConvertDragDropEffects(operation));
}
if(_sourceHandle != nil)
{
FreeAvnGCHandle(_sourceHandle);
_sourceHandle = nil;
}
}
- (void*) gcHandle
{
return _sourceHandle;
}
@end
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle)
{
return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle];
};
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info)
{
id obj = [info draggingSource];
if(obj == nil)
return nil;
if([obj isKindOfClass: [AvnDndSource class]])
{
auto src = (AvnDndSource*)obj;
return [src gcHandle];
}
return nil;
}

19
native/Avalonia.Native/src/OSX/main.mm

@ -150,14 +150,15 @@ public:
}
@end
static ComPtr<IAvnGCHandleDeallocatorCallback> _deallocator;
class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAvaloniaNativeFactory>
{
public:
FORWARD_IUNKNOWN()
virtual HRESULT Initialize() override
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
{
_deallocator = deallocator;
@autoreleasepool{
[[ThreadingInitializer new] do];
}
@ -207,7 +208,13 @@ public:
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override
{
*ppv = ::CreateClipboard ();
*ppv = ::CreateClipboard (nil, nil);
return S_OK;
}
virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override
{
*ppv = ::CreateClipboard (nil, [NSPasteboardItem new]);
return S_OK;
}
@ -257,6 +264,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()
return new AvaloniaNative();
};
extern void FreeAvnGCHandle(void* handle)
{
if(_deallocator != nil)
_deallocator->FreeGCHandle(handle);
}
NSSize ToNSSize (AvnSize s)
{
NSSize result;

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

@ -3,7 +3,7 @@
class WindowBaseImpl;
@interface AvnView : NSView<NSTextInputClient>
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt;

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

@ -382,6 +382,50 @@ public:
*ppv = [renderTarget createSurfaceRenderTarget];
return *ppv == nil ? E_FAIL : S_OK;
}
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb,
void* sourceHandle) override
{
auto item = TryGetPasteboardItem(clipboard);
[item setString:@"" forType:GetAvnCustomDataType()];
if(item == nil)
return E_INVALIDARG;
if(View == NULL)
return E_FAIL;
auto nsevent = [NSApp currentEvent];
auto nseventType = [nsevent type];
// If current event isn't a mouse one (probably due to malfunctioning user app)
// attempt to forge a new one
if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited)
|| (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged)))
{
auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)];
CGPoint cgpoint = NSPointToCGPoint(nspoint);
auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft);
nsevent = [NSEvent eventWithCGEvent: cgevent];
CFRelease(cgevent);
}
auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item];
auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments];
NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height};
[dragItem setDraggingFrame: dragItemRect contents: dragItemImage];
int op = 0; int ieffects = (int)effects;
if((ieffects & (int)AvnDragDropEffects::Copy) != 0)
op |= NSDragOperationCopy;
if((ieffects & (int)AvnDragDropEffects::Link) != 0)
op |= NSDragOperationLink;
if((ieffects & (int)AvnDragDropEffects::Move) != 0)
op |= NSDragOperationMove;
[View beginDraggingSessionWithItems: @[dragItem] event: nsevent
source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)];
return S_OK;
}
protected:
virtual NSWindowStyleMask GetStyle()
@ -911,7 +955,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_area = nullptr;
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
return self;
}
@ -1302,6 +1346,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return result;
}
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
{
auto localPoint = [self convertPoint:[info draggingLocation] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]];
NSDragOperation nsop = [info draggingSourceOperationMask];
auto effects = ConvertDragDropEffects(nsop);
int reffects = (int)_parent->BaseEvents
->DragEvent(type, point, modifiers, effects,
CreateClipboard([info draggingPasteboard], nil),
GetAvnDataObjectHandleFromDraggingInfo(info));
NSDragOperation ret = 0;
// Ensure that the managed part didn't add any new effects
reffects = (int)effects & (int)reffects;
// OSX requires exactly one operation
if((reffects & (int)AvnDragDropEffects::Copy) != 0)
ret = NSDragOperationCopy;
else if((reffects & (int)AvnDragDropEffects::Move) != 0)
ret = NSDragOperationMove;
else if((reffects & (int)AvnDragDropEffects::Link) != 0)
ret = NSDragOperationLink;
if(ret == 0)
ret = NSDragOperationNone;
return ret;
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender];
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender];
}
- (void)draggingExited:(id <NSDraggingInfo>)sender
{
[self triggerAvnDragEvent: AvnDragEventType::Leave info:sender];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone;
}
- (void)concludeDragOperation:(nullable id <NSDraggingInfo>)sender
{
}
@end

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)

5
packages/Avalonia/AvaloniaBuildTasks.targets

@ -2,6 +2,7 @@
<PropertyGroup>
<_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild>
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
</PropertyGroup>
<UsingTask TaskName="GenerateAvaloniaResourcesTask"
@ -38,7 +39,8 @@
Output="$(AvaloniaResourcesTemporaryFilePath)"
Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)"
EmbeddedResources="@(EmbeddedResources)"/>
EmbeddedResources="@(EmbeddedResources)"
ReportImportance="$(AvaloniaXamlReportImportance)"/>
<Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
@ -67,6 +69,7 @@
OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)"
ProjectDirectory="$(MSBuildProjectDirectory)"
VerifyIl="$(AvaloniaXamlIlVerifyIl)"
ReportImportance="$(AvaloniaXamlReportImportance)"
/>
<Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"

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

2
samples/ControlCatalog.NetCore/Program.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@ -6,6 +7,7 @@ using System.Threading;
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Dialogs;
using Avalonia.OpenGL;
namespace ControlCatalog.NetCore
{

4
samples/ControlCatalog/ControlCatalog.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
@ -17,6 +18,7 @@
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Italic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Regular.ttf" />
<EmbeddedResource Include="Pages\teapot.bin" />
</ItemGroup>
<ItemGroup>

1
samples/ControlCatalog/MainView.xaml

@ -47,6 +47,7 @@
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="OpenGL"><pages:OpenGlPage/></TabItem>
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>

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();

12
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -9,9 +9,15 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
<TextBlock Name="DragState">Drag Me</TextBlock>
</Border>
<StackPanel>
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMeText">
<TextBlock Name="DragStateText">Drag Me</TextBlock>
</Border>
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMeCustom">
<TextBlock Name="DragStateCustom">Drag Me (custom)</TextBlock>
</Border>
</StackPanel>
<Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16"
DragDrop.AllowDrop="True">
<TextBlock Name="DropState">Drop some text or files here</TextBlock>

92
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -3,69 +3,85 @@ using Avalonia.Input;
using Avalonia.Markup.Xaml;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ControlCatalog.Pages
{
public class DragAndDropPage : UserControl
{
private TextBlock _DropState;
private TextBlock _DragState;
private Border _DragMe;
private int DragCount = 0;
TextBlock _DropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Find<TextBlock>("DropState");
_DragMe.PointerPressed += DoDrag;
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"));
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"));
}
private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects = DragDropEffects.Copy)
{
DataObject dragData = new DataObject();
dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
var dragMe = this.Find<Border>("DragMe" + suffix);
var dragState = this.Find<TextBlock>("DragState"+suffix);
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
switch(result)
async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
case DragDropEffects.Copy:
_DragState.Text = "The text was copied"; break;
case DragDropEffects.Link:
_DragState.Text = "The text was linked"; break;
case DragDropEffects.None:
_DragState.Text = "The drag operation was canceled"; break;
var dragData = new DataObject();
factory(dragData);
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
switch (result)
{
case DragDropEffects.Copy:
dragState.Text = "Data was copied";
break;
case DragDropEffects.Link:
dragState.Text = "Data was linked";
break;
case DragDropEffects.None:
dragState.Text = "The drag operation was canceled";
break;
}
}
}
private void DragOver(object sender, DragEventArgs e)
{
// Only allow Copy or Link as Drop Operations.
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
void DragOver(object sender, DragEventArgs e)
{
// Only allow Copy or Link as Drop Operations.
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames))
e.DragEffects = DragDropEffects.None;
}
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames)
&& !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
private void Drop(object sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
void Drop(object sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
else if (e.Data.Contains(CustomFormat))
_DropState.Text = "Custom: " + e.Data.Get(CustomFormat);
}
dragMe.PointerPressed += DoDrag;
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_DropState = this.Find<TextBlock>("DropState");
_DragState = this.Find<TextBlock>("DragState");
_DragMe = this.Find<Border>("DragMe");
}
}
}

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>

29
samples/ControlCatalog/Pages/OpenGlPage.xaml

@ -0,0 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.OpenGlPage"
xmlns:pages="clr-namespace:ControlCatalog.Pages">
<Grid>
<pages:OpenGlPageControl x:Name="GL"/>
<StackPanel>
<TextBlock Text="{Binding #GL.Info}"/>
</StackPanel>
<Grid ColumnDefinitions="*,Auto" Margin="20">
<StackPanel Grid.Column="1" MinWidth="300">
<TextBlock>Yaw</TextBlock>
<Slider Value="{Binding Yaw, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<TextBlock>Pitch</TextBlock>
<Slider Value="{Binding Pitch, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<TextBlock>Roll</TextBlock>
<Slider Value="{Binding Roll, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Foreground="#C000C0">D</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#00C090">I</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#90C000">S</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#C09000">C</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#00C090">O</TextBlock>
</StackPanel>
<Slider Value="{Binding Disco, Mode=TwoWay, ElementName=GL}" Maximum="1"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

401
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@ -0,0 +1,401 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.OpenGL;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using static Avalonia.OpenGL.GlConsts;
// ReSharper disable StringLiteralTypo
namespace ControlCatalog.Pages
{
public class OpenGlPage : UserControl
{
}
public class OpenGlPageControl : OpenGlControlBase
{
private float _yaw;
public static readonly DirectProperty<OpenGlPageControl, float> YawProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Yaw", o => o.Yaw, (o, v) => o.Yaw = v);
public float Yaw
{
get => _yaw;
set => SetAndRaise(YawProperty, ref _yaw, value);
}
private float _pitch;
public static readonly DirectProperty<OpenGlPageControl, float> PitchProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Pitch", o => o.Pitch, (o, v) => o.Pitch = v);
public float Pitch
{
get => _pitch;
set => SetAndRaise(PitchProperty, ref _pitch, value);
}
private float _roll;
public static readonly DirectProperty<OpenGlPageControl, float> RollProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Roll", o => o.Roll, (o, v) => o.Roll = v);
public float Roll
{
get => _roll;
set => SetAndRaise(RollProperty, ref _roll, value);
}
private float _disco;
public static readonly DirectProperty<OpenGlPageControl, float> DiscoProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Disco", o => o.Disco, (o, v) => o.Disco = v);
public float Disco
{
get => _disco;
set => SetAndRaise(DiscoProperty, ref _disco, value);
}
private string _info;
public static readonly DirectProperty<OpenGlPageControl, string> InfoProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, string>("Info", o => o.Info, (o, v) => o.Info = v);
public string Info
{
get => _info;
private set => SetAndRaise(InfoProperty, ref _info, value);
}
static OpenGlPageControl()
{
AffectsRender<OpenGlPageControl>(YawProperty, PitchProperty, RollProperty, DiscoProperty);
}
private int _vertexShader;
private int _fragmentShader;
private int _shaderProgram;
private int _vertexBufferObject;
private int _indexBufferObject;
private int _vertexArrayObject;
private GlExtrasInterface _glExt;
private string GetShader(bool fragment, string shader)
{
var version = (GlVersion.Type == GlProfileType.OpenGL ?
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 :
100);
var data = "#version " + version + "\n";
if (GlVersion.Type == GlProfileType.OpenGLES)
data += "precision mediump float;\n";
if (version >= 150)
{
shader = shader.Replace("attribute", "in");
if (fragment)
shader = shader
.Replace("varying", "in")
.Replace("//DECLAREGLFRAG", "out vec4 outFragColor;")
.Replace("gl_FragColor", "outFragColor");
else
shader = shader.Replace("varying", "out");
}
data += shader;
return data;
}
private string VertexShaderSource => GetShader(false, @"
attribute vec3 aPos;
attribute vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uProjection;
uniform mat4 uView;
varying vec3 FragPos;
varying vec3 VecPos;
varying vec3 Normal;
uniform float uTime;
uniform float uDisco;
void main()
{
float discoScale = sin(uTime * 10.0) / 10.0;
float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0;
float scale = 1.0 + uDisco * discoScale;
vec3 scaledPos = aPos;
scaledPos.x = scaledPos.x * distortionX;
scaledPos *= scale;
gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0);
FragPos = vec3(uModel * vec4(aPos, 1.0));
VecPos = aPos;
Normal = normalize(vec3(uModel * vec4(aNormal, 1.0)));
}
");
private string FragmentShaderSource => GetShader(true, @"
varying vec3 FragPos;
varying vec3 VecPos;
varying vec3 Normal;
uniform float uMaxY;
uniform float uMinY;
uniform float uTime;
uniform float uDisco;
//DECLAREGLFRAG
void main()
{
float y = (VecPos.y - uMinY) / (uMaxY - uMinY);
float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0);
float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0);
vec3 discoColor = vec3(
0.5 + abs(0.5 - y) * cos(uTime * 10.0),
0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)),
0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0))));
vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25);
objectColor = objectColor * (1.0 - uDisco) + discoColor * uDisco;
float ambientStrength = 0.3;
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 2.0);
vec3 ambient = ambientStrength * lightColor;
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
gl_FragColor = vec4(result, 1.0);
}
");
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
}
private readonly Vertex[] _points;
private readonly ushort[] _indices;
private readonly float _minY;
private readonly float _maxY;
public OpenGlPageControl()
{
var name = typeof(OpenGlPage).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin"));
using (var sr = new BinaryReader(typeof(OpenGlPage).Assembly.GetManifestResourceStream(name)))
{
var buf = new byte[sr.ReadInt32()];
sr.Read(buf, 0, buf.Length);
var points = new float[buf.Length / 4];
Buffer.BlockCopy(buf, 0, points, 0, buf.Length);
buf = new byte[sr.ReadInt32()];
sr.Read(buf, 0, buf.Length);
_indices = new ushort[buf.Length / 2];
Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length);
_points = new Vertex[points.Length / 3];
for (var primitive = 0; primitive < points.Length / 3; primitive++)
{
var srci = primitive * 3;
_points[primitive] = new Vertex
{
Position = new Vector3(points[srci], points[srci + 1], points[srci + 2])
};
}
for (int i = 0; i < _indices.Length; i += 3)
{
Vector3 a = _points[_indices[i]].Position;
Vector3 b = _points[_indices[i + 1]].Position;
Vector3 c = _points[_indices[i + 2]].Position;
var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b));
_points[_indices[i]].Normal += normal;
_points[_indices[i + 1]].Normal += normal;
_points[_indices[i + 2]].Normal += normal;
}
for (int i = 0; i < _points.Length; i++)
{
_points[i].Normal = Vector3.Normalize(_points[i].Normal);
_maxY = Math.Max(_maxY, _points[i].Position.Y);
_minY = Math.Min(_minY, _points[i].Position.Y);
}
}
}
private void CheckError(GlInterface gl)
{
int err;
while ((err = gl.GetError()) != GL_NO_ERROR)
Console.WriteLine(err);
}
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb)
{
CheckError(GL);
_glExt = new GlExtrasInterface(GL);
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}";
// Load the source of the vertex shader and compile it.
_vertexShader = GL.CreateShader(GL_VERTEX_SHADER);
Console.WriteLine(GL.CompileShaderAndGetError(_vertexShader, VertexShaderSource));
// Load the source of the fragment shader and compile it.
_fragmentShader = GL.CreateShader(GL_FRAGMENT_SHADER);
Console.WriteLine(GL.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource));
// Create the shader program, attach the vertex and fragment shaders and link the program.
_shaderProgram = GL.CreateProgram();
GL.AttachShader(_shaderProgram, _vertexShader);
GL.AttachShader(_shaderProgram, _fragmentShader);
const int positionLocation = 0;
const int normalLocation = 1;
GL.BindAttribLocationString(_shaderProgram, positionLocation, "aPos");
GL.BindAttribLocationString(_shaderProgram, normalLocation, "aNormal");
Console.WriteLine(GL.LinkProgramAndGetError(_shaderProgram));
CheckError(GL);
// Create the vertex buffer object (VBO) for the vertex data.
_vertexBufferObject = GL.GenBuffer();
// Bind the VBO and copy the vertex data into it.
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
CheckError(GL);
var vertexSize = Marshal.SizeOf<Vertex>();
fixed (void* pdata = _points)
GL.BufferData(GL_ARRAY_BUFFER, new IntPtr(_points.Length * vertexSize),
new IntPtr(pdata), GL_STATIC_DRAW);
_indexBufferObject = GL.GenBuffer();
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
CheckError(GL);
fixed (void* pdata = _indices)
GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata),
GL_STATIC_DRAW);
CheckError(GL);
_vertexArrayObject = _glExt.GenVertexArray();
_glExt.BindVertexArray(_vertexArrayObject);
CheckError(GL);
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT,
0, vertexSize, IntPtr.Zero);
GL.VertexAttribPointer(normalLocation, 3, GL_FLOAT,
0, vertexSize, new IntPtr(12));
GL.EnableVertexAttribArray(positionLocation);
GL.EnableVertexAttribArray(normalLocation);
CheckError(GL);
}
protected override void OnOpenGlDeinit(GlInterface GL, int fb)
{
// Unbind everything
GL.BindBuffer(GL_ARRAY_BUFFER, 0);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
_glExt.BindVertexArray(0);
GL.UseProgram(0);
// Delete all resources.
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject });
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject });
GL.DeleteProgram(_shaderProgram);
GL.DeleteShader(_fragmentShader);
GL.DeleteShader(_vertexShader);
}
static Stopwatch St = Stopwatch.StartNew();
protected override unsafe void OnOpenGlRender(GlInterface gl, int fb)
{
gl.ClearColor(0, 0, 0, 0);
gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
gl.Enable(GL_DEPTH_TEST);
gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height);
var GL = gl;
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
_glExt.BindVertexArray(_vertexArrayObject);
GL.UseProgram(_shaderProgram);
CheckError(GL);
var projection =
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)(Bounds.Width / Bounds.Height),
0.01f, 1000);
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0));
var model = Matrix4x4.CreateFromYawPitchRoll(_yaw, _pitch, _roll);
var modelLoc = GL.GetUniformLocationString(_shaderProgram, "uModel");
var viewLoc = GL.GetUniformLocationString(_shaderProgram, "uView");
var projectionLoc = GL.GetUniformLocationString(_shaderProgram, "uProjection");
var maxYLoc = GL.GetUniformLocationString(_shaderProgram, "uMaxY");
var minYLoc = GL.GetUniformLocationString(_shaderProgram, "uMinY");
var timeLoc = GL.GetUniformLocationString(_shaderProgram, "uTime");
var discoLoc = GL.GetUniformLocationString(_shaderProgram, "uDisco");
GL.UniformMatrix4fv(modelLoc, 1, false, &model);
GL.UniformMatrix4fv(viewLoc, 1, false, &view);
GL.UniformMatrix4fv(projectionLoc, 1, false, &projection);
GL.Uniform1f(maxYLoc, _maxY);
GL.Uniform1f(minYLoc, _minY);
GL.Uniform1f(timeLoc, (float)St.Elapsed.TotalSeconds);
GL.Uniform1f(discoLoc, _disco);
CheckError(GL);
GL.DrawElements(GL_TRIANGLES, _indices.Length, GL_UNSIGNED_SHORT, IntPtr.Zero);
CheckError(GL);
if (_disco > 0.01)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo>
{
public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo)
{
}
public delegate void GlDeleteVertexArrays(int count, int[] buffers);
[GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)]
[GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")]
public GlDeleteVertexArrays DeleteVertexArrays { get; }
public delegate void GlBindVertexArray(int array);
[GlMinVersionEntryPoint("glBindVertexArray", 3,0)]
[GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")]
public GlBindVertexArray BindVertexArray { get; }
public delegate void GlGenVertexArrays(int n, int[] rv);
[GlMinVersionEntryPoint("glGenVertexArrays",3,0)]
[GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")]
public GlGenVertexArrays GenVertexArrays { get; }
public int GenVertexArray()
{
var rv = new int[1];
GenVertexArrays(1, rv);
return rv[0];
}
}
}
}

3
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}"/>
@ -19,7 +19,6 @@
</TreeView>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">

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

@ -1,9 +1,6 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@ -12,104 +9,12 @@ namespace ControlCatalog.Pages
public TreeViewPage()
{
InitializeComponent();
DataContext = new PageViewModel();
DataContext = new TreeViewPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class PageViewModel : ReactiveObject
{
private SelectionMode _selectionMode;
public PageViewModel()
{
Node root = new Node();
Items = root.Children;
SelectedItems = new ObservableCollection<Node>();
AddItemCommand = ReactiveCommand.Create(() =>
{
Node parentItem = SelectedItems.Count > 0 ? SelectedItems[0] : root;
parentItem.AddNewItem();
});
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (SelectedItems.Count > 0)
{
Node lastItem = SelectedItems[0];
RecursiveRemove(Items, lastItem);
SelectedItems.Remove(lastItem);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
{
if (items.Remove(selectedItem))
{
return true;
}
foreach (Node item in items)
{
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
{
return true;
}
}
return false;
}
});
}
public ObservableCollection<Node> Items { get; }
public ObservableCollection<Node> SelectedItems { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
SelectedItems.Clear();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
}
private class Node
{
private int _counter;
private ObservableCollection<Node> _children;
public string Header { get; private set; }
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children
{
get
{
if (_children == null)
{
_children = new ObservableCollection<Node>(Enumerable.Range(1, 10).Select(i => CreateNewNode()));
}
return _children;
}
}
public void AddNewItem() => Children.Add(CreateNewNode());
public override string ToString() => Header;
private Node CreateNewNode() => new Node {Header = $"Item {_counter++}"};
}
}
}

BIN
samples/ControlCatalog/Pages/teapot.bin

Binary file not shown.

115
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
public class TreeViewPageViewModel : ReactiveObject
{
private readonly Node _root;
private SelectionMode _selectionMode;
public TreeViewPageViewModel()
{
_root = new Node();
Items = _root.Children;
Selection = new SelectionModel();
Selection.SelectionChanged += SelectionChanged;
AddItemCommand = ReactiveCommand.Create(AddItem);
RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
}
public ObservableCollection<Node> Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
Selection.ClearSelection();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private void AddItem()
{
var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root;
parentItem.AddItem();
}
private void RemoveItem()
{
while (Selection.SelectedItems.Count > 0)
{
Node lastItem = (Node)Selection.SelectedItems[0];
RecursiveRemove(Items, lastItem);
Selection.DeselectAt(Selection.SelectedIndices[0]);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
{
if (items.Remove(selectedItem))
{
return true;
}
foreach (Node item in items)
{
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
{
return true;
}
}
return false;
}
}
private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
var selected = string.Join(",", e.SelectedIndices);
var deselected = string.Join(",", e.DeselectedIndices);
System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'");
}
public class Node
{
private ObservableCollection<Node> _children;
private int _childIndex = 10;
public Node()
{
Header = "Item";
}
public Node(Node parent, int index)
{
Parent = parent;
Header = parent.Header + ' ' + index;
}
public Node Parent { get; }
public string Header { get; }
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children => _children ??= CreateChildren();
public void AddItem() => Children.Add(new Node(this, _childIndex++));
public void RemoveItem(Node child) => Children.Remove(child);
public override string ToString() => Header;
private ObservableCollection<Node> CreateChildren()
{
return new ObservableCollection<Node>(
Enumerable.Range(0, 10).Select(i => new Node(this, i)));
}
}
}
}

53
samples/RenderDemo/Controls/LineBoundsDemoControl.cs

@ -0,0 +1,53 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
namespace RenderDemo.Controls
{
public class LineBoundsDemoControl : Control
{
static LineBoundsDemoControl()
{
AffectsRender<LineBoundsDemoControl>(AngleProperty);
}
public LineBoundsDemoControl()
{
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1 / 60.0);
timer.Tick += (sender, e) => Angle += Math.PI / 360;
timer.Start();
}
public static readonly StyledProperty<double> AngleProperty =
AvaloniaProperty.Register<LineBoundsDemoControl, double>(nameof(Angle));
public double Angle
{
get => GetValue(AngleProperty);
set => SetValue(AngleProperty, value);
}
public override void Render(DrawingContext drawingContext)
{
var lineLength = Math.Sqrt((100 * 100) + (100 * 100));
var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength);
var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength);
var p1 = new Point(200, 200);
var p2 = new Point(p1.X + diffX, p1.Y + diffY);
var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square);
var boundPen = new Pen(Brushes.Black);
drawingContext.DrawLine(pen, p1, p2);
drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen));
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -44,6 +44,9 @@
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>
</TabControl>
</DockPanel>
</Window>

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>

9
samples/RenderDemo/Pages/LineBoundsPage.xaml

@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:RenderDemo.Controls"
x:Class="RenderDemo.Pages.LineBoundsPage">
<controls:LineBoundsDemoControl />
</UserControl>

19
samples/RenderDemo/Pages/LineBoundsPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace RenderDemo.Pages
{
public class LineBoundsPage : UserControl
{
public LineBoundsPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

3
samples/RenderDemo/RenderDemo.csproj

@ -3,6 +3,9 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.Visuals\Rendering\SceneGraph\LineBoundsHelper.cs" Link="LineBoundsHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />

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);
}
}
}

10
scripts/ReplaceNugetCache.ps1

@ -1,5 +1,5 @@
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

10
scripts/ReplaceNugetCacheRelease.ps1

@ -1,5 +1,5 @@
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

50
src/Avalonia.Base/Utilities/DisposableLock.cs

@ -0,0 +1,50 @@
using System;
using System.Threading;
namespace Avalonia.Utilities
{
public class DisposableLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to take a lock
/// </summary>
/// <returns>IDisposable if succeeded to obtain the lock</returns>
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return new UnlockDisposable(_lock);
return null;
}
/// <summary>
/// Enters a waiting lock
/// </summary>
public IDisposable Lock()
{
Monitor.Enter(_lock);
return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
}
}
}

11
src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs

@ -12,6 +12,8 @@ namespace Avalonia.Build.Tasks
{
public bool Execute()
{
Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance);
OutputPath = OutputPath ?? AssemblyFile;
var outputPdb = GetPdbPath(OutputPath);
var input = AssemblyFile;
@ -32,9 +34,12 @@ namespace Avalonia.Build.Tasks
}
}
var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance);
var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
ProjectDirectory, OutputPath, VerifyIl);
ProjectDirectory, OutputPath, VerifyIl, outputImportance);
if (!res.Success)
return false;
if (!res.WrittenFile)
@ -68,7 +73,9 @@ namespace Avalonia.Build.Tasks
public string OutputPath { get; set; }
public bool VerifyIl { get; set; }
public string ReportImportance { get; set; }
public IBuildEngine BuildEngine { get; set; }
public ITaskHost HostObject { get; set; }
}

9
src/Avalonia.Build.Tasks/Extensions.cs

@ -9,14 +9,19 @@ namespace Avalonia.Build.Tasks
public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
{
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
"", "Avalonia"));
}
public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
{
engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
"", "Avalonia"));
}
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp)
{
engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp));
}
}
}

31
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks
[Required]
public ITaskItem[] EmbeddedResources { get; set; }
public string ReportImportance { get; set; }
private MessageImportance _reportImportance;
class Source
{
public string Path { get; set; }
@ -29,15 +33,11 @@ namespace Avalonia.Build.Tasks
private byte[] _data;
private string _sourcePath;
public Source(string file, string root)
public Source(string relativePath, string root)
{
file = SPath.GetFullPath(file);
root = SPath.GetFullPath(root);
var fileUri = new Uri(file, UriKind.Absolute);
var rootUri = new Uri(root, UriKind.Absolute);
rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/');
Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/');
_sourcePath = file;
Path = "/" + relativePath.Replace('\\', '/');
_sourcePath = SPath.Combine(root, relativePath);
Size = (int)new FileInfo(_sourcePath).Length;
}
@ -65,7 +65,14 @@ namespace Avalonia.Build.Tasks
}
}
List<Source> BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList();
List<Source> BuildResourceSources()
=> Resources.Select(r =>
{
var src = new Source(r.ItemSpec, Root);
BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance);
return src;
}).ToList();
private void Pack(Stream output, List<Source> sources)
{
@ -136,10 +143,14 @@ namespace Avalonia.Build.Tasks
sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray()));
return true;
}
public bool Execute()
{
foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml")))
Enum.TryParse<MessageImportance>(ReportImportance, out _reportImportance);
BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml")))
BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources();

4
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks
}
public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory,
string output, bool verifyIl)
string output, bool verifyIl, MessageImportance logImportance)
{
var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input);
var asm = typeSystem.TargetAssemblyDefinition;
@ -121,6 +121,8 @@ namespace Avalonia.Build.Tasks
{
try
{
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
// StreamReader is needed here to handle BOM
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlIlParser.Parse(xaml);

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

@ -2245,7 +2245,7 @@ namespace Avalonia.Controls
/// Builds the visual tree for the column header when a new template is applied.
/// </summary>
//TODO Validation UI
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
// The template has changed, so we need to refresh the visuals
_measured = false;

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

@ -121,10 +121,8 @@ namespace Avalonia.Controls
/// <summary>
/// Builds the visual tree for the cell control when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
UpdatePseudoClasses();
_rightGridLine = e.NameScope.Find<Rectangle>(DATAGRIDCELL_elementRightGridLine);
if (_rightGridLine != null && OwningColumn == null)
@ -164,7 +162,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;
}

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

@ -536,10 +536,8 @@ namespace Avalonia.Controls
/// <summary>
/// Builds the visual tree for the column header when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot);
if (RootElement != null)
{
@ -786,7 +784,7 @@ namespace Avalonia.Controls
private void DataGridRow_PointerPressed(PointerPressedEventArgs e)
{
if(e.MouseButton != MouseButton.Left)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
return;
}

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

@ -168,7 +168,7 @@ namespace Avalonia.Controls
private IDisposable _expanderButtonSubscription;
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_rootElement = e.NameScope.Find<Panel>(DataGridRow.DATAGRIDROW_elementRoot);
@ -199,8 +199,6 @@ namespace Avalonia.Controls
_itemCountElement = e.NameScope.Find<TextBlock>(DATAGRIDROWGROUPHEADER_itemCountElement);
_propertyNameElement = e.NameScope.Find<TextBlock>(DATAGRIDROWGROUPHEADER_propertyNameElement);
UpdateTitleElements();
base.OnTemplateApplied(e);
}
internal void ApplyHeaderStatus()
@ -277,7 +275,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)
{

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

@ -94,10 +94,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Builds the visual tree for the row header when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
_rootElement = e.NameScope.Find<Control>(DATAGRIDROWHEADER_elementRootName);
if (_rootElement != null)
{
@ -164,7 +162,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;
}

8
src/Avalonia.Controls/Application.cs

@ -254,8 +254,12 @@ namespace Avalonia
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
.Bind<IStyler>().ToConstant(_styler)
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance)
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance);
// TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x
if (AvaloniaLocator.Current.GetService<IPlatformDragSource>() == null)
AvaloniaLocator.CurrentMutable
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
var clock = new RenderLoopClock();
AvaloniaLocator.CurrentMutable

6
src/Avalonia.Controls/AutoCompleteBox.cs

@ -10,6 +10,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
@ -1100,6 +1101,7 @@ namespace Avalonia.Controls
{
_textBoxSubscriptions =
_textBox.GetObservable(TextBox.TextProperty)
.Skip(1)
.Subscribe(_ => OnTextBoxTextChanged());
if (Text != null)
@ -1212,7 +1214,7 @@ namespace Avalonia.Controls
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control
/// when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (DropDownPopup != null)
@ -1240,7 +1242,7 @@ namespace Avalonia.Controls
OpeningDropDown(false);
}
base.OnTemplateApplied(e);
base.OnApplyTemplate(e);
}
/// <summary>

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;

2
src/Avalonia.Controls/ButtonSpinner.cs

@ -121,7 +121,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc />
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
IncreaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
DecreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");

4
src/Avalonia.Controls/Calendar/Calendar.cs

@ -2079,10 +2079,8 @@ namespace Avalonia.Controls
/// <see cref="T:System.Windows.Controls.Calendar" /> when a new
/// template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
Root = e.NameScope.Find<Panel>(PART_ElementRoot);
SelectedMonth = DisplayDate;

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

@ -98,9 +98,8 @@ namespace Avalonia.Controls.Primitives
/// <see cref="T:System.Windows.Controls.Primitives.CalendarButton" />
/// when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
SetPseudoClasses();
}
@ -149,7 +148,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);
}

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

@ -150,11 +150,11 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
SetPseudoClasses();
}
private void SetPseudoClasses()
{
if (_ignoringMouseOverState)
@ -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);
}

4
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -268,10 +268,8 @@ namespace Avalonia.Controls.Primitives
/// <see cref="T:System.Windows.Controls.Primitives.CalendarItem" />
/// when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderButton = e.NameScope.Find<Button>(PART_ElementHeaderButton);
PreviousButton = e.NameScope.Find<Button>(PART_ElementPreviousButton);
NextButton = e.NameScope.Find<Button>(PART_ElementNextButton);

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

@ -413,7 +413,7 @@ namespace Avalonia.Controls
DisplayDate = DateTime.Today;
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_calendar != null)
{
@ -508,8 +508,6 @@ namespace Avalonia.Controls
SetSelectedDate();
}
}
base.OnTemplateApplied(e);
}
protected override void OnPropertyChanged<T>(
@ -839,7 +837,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;
}

8
src/Avalonia.Controls/ComboBox.cs

@ -219,7 +219,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_popup != null)
{
@ -230,8 +230,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
base.OnTemplateApplied(e);
}
/// <summary>
@ -306,9 +304,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);
}

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls
{
/// <summary>
/// A control context menu.
/// </summary>
public class ContextMenu : MenuBase
public class ContextMenu : MenuBase, ISetterValue
{
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private Control _attachedControl;
private List<Control> _attachedControls;
private IInputElement _previousFocus;
/// <summary>
@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControl = control;
newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased;
}
}
@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control is null && _attachedControl is null)
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
throw new ArgumentNullException(nameof(control));
}
if (control is object && _attachedControl is object && control != _attachedControl)
if (control is object &&
_attachedControls is object &&
!_attachedControls.Contains(control))
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
control ??= _attachedControls[0];
if (IsOpen)
{
return;
@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
((ISetLogicalParent)_popup).SetParent(control);
if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this;
_popup.IsOpen = true;
@ -155,6 +167,17 @@ namespace Avalonia.Controls
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1;
IsOpen = false;
if (_attachedControl is null)
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
}

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);
}
}
}
}
}

22
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()
@ -146,10 +161,9 @@ namespace Avalonia.Controls
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
base.OnTemplateApplied(e);
}
}
}

4
src/Avalonia.Controls/MenuItem.cs

@ -417,10 +417,8 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
if (_popup != null)
{
_popup.Opened -= PopupOpened;

4
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -77,10 +77,8 @@ namespace Avalonia.Controls.Notifications
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
var itemsControl = e.NameScope.Find<Panel>("PART_Items");
_items = itemsControl?.Children;
}

2
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -295,7 +295,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc />
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (TextBox != null)
{

4
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)
{

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)
{
}

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

@ -147,10 +147,8 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
if (_lineUpButton != null)
{
_lineUpButton.Click -= LineUpClick;

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

File diff suppressed because it is too large

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

@ -263,7 +263,10 @@ namespace Avalonia.Controls.Primitives
if (nameScope == null)
nameScope = new NameScope();
OnTemplateApplied(new TemplateAppliedEventArgs(nameScope));
var e = new TemplateAppliedEventArgs(nameScope);
OnApplyTemplate(e);
OnTemplateApplied(e);
RaiseEvent(e);
}
_appliedTemplate = template;
@ -306,13 +309,17 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e);
}
protected virtual void OnApplyTemplate(TemplateAppliedEventArgs e)
{
}
/// <summary>
/// Called when the control's template is applied.
/// </summary>
/// <param name="e">The event args.</param>
[Obsolete("Use OnApplyTemplate")]
protected virtual void OnTemplateApplied(TemplateAppliedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>

2
src/Avalonia.Controls/ProgressBar.cs

@ -102,7 +102,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_indicator = e.NameScope.Get<Border>("PART_Indicator");

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();
}

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

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -379,14 +380,19 @@ namespace Avalonia.Controls
{
if (property == ItemsProperty)
{
var oldEnumerable = oldValue.GetValueOrDefault<IEnumerable>();
var newEnumerable = newValue.GetValueOrDefault<IEnumerable>();
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
if (oldEnumerable != newEnumerable)
{
newDataSource = new ItemsSourceView(newEnumerable);
}
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
}
else if (property == ItemTemplateProperty)
{
@ -431,8 +437,16 @@ namespace Avalonia.Controls
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
// Verify that element is actually a child of this ItemsRepeater
var parent = element.GetVisualParent();
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
return -1;
}
private IControl GetElementFromIndexImpl(int index)

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>

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

@ -388,19 +388,24 @@ namespace Avalonia.Controls
}
case NotifyCollectionChangedAction.Reset:
if (_owner.ItemsSourceView.HasKeyIndexMapping)
// If we get multiple resets back to back before
// running layout, we dont have to clear all the elements again.
if (!_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = true;
}
if (_owner.ItemsSourceView.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
_owner.ClearElementImpl(element);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
}
@ -441,6 +446,9 @@ namespace Avalonia.Controls
}
_resetPool.Clear();
// Flush the realized indices once the stable reset pool is cleared to start fresh.
InvalidateRealizedIndicesHeldByLayout();
}
}
@ -498,6 +506,10 @@ namespace Avalonia.Controls
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
}
}
@ -519,6 +531,10 @@ namespace Avalonia.Controls
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break;
}
}

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();
}
}

864
src/Avalonia.Controls/SelectionModel.cs

@ -0,0 +1,864 @@
// 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 IndexPath _oldAnchorIndex;
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
{
var oldValue = AnchorIndex;
if (value != null)
{
SelectionTreeHelper.TraverseIndexPath(
_rootNode,
value,
realizeChildren: true,
(currentNode, path, depth, childIndex) => currentNode.AnchorIndex = path.GetAt(depth));
}
else
{
_rootNode.AnchorIndex = -1;
}
if (_operationCount == 0 && oldValue != AnchorIndex)
{
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;
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 =>
{
if (info.Path >= winrtStart && info.Path <= winrtEnd)
{
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
}
});
}
private void BeginOperation()
{
if (_operationCount++ == 0)
{
_oldAnchorIndex = AnchorIndex;
_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);
if (_oldAnchorIndex != AnchorIndex)
{
RaisePropertyChanged(nameof(AnchorIndex));
}
_rootNode.Cleanup();
_oldAnchorIndex = default;
}
}
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; }
}
}

950
src/Avalonia.Controls/SelectionNode.cs

@ -0,0 +1,950 @@
// 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;
using Avalonia.Controls.Utils;
#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 =>
{
if (Source != null)
{
using (_manager.Update())
{
SelectionTreeHelper.Traverse(
this,
realizeChildren: false,
info => info.Node.Clear());
}
}
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()
{
for (int i = 0; i < _childrenNodes.Count; i++)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.Dispose();
_childrenNodes[i] = null;
}
}
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;
var 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;
}
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/Slider.cs

@ -82,7 +82,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_decreaseButton != null)
{

6
src/Avalonia.Controls/TabControl.cs

@ -217,10 +217,8 @@ namespace Avalonia.Controls
return new TabItemContainerGenerator(this);
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
}
@ -240,7 +238,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);
}

2
src/Avalonia.Controls/TextBox.cs

@ -341,7 +341,7 @@ namespace Avalonia.Controls
set { SetAndRaise(NewLineProperty, ref _newLine, value); }
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");

671
src/Avalonia.Controls/TreeView.cs

@ -2,11 +2,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Collections;
using System.Reactive.Linq;
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 +44,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 +76,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 +107,6 @@ namespace Avalonia.Controls
set => SetValue(AutoScrollToSelectedItemProperty, value);
}
private bool _syncingSelectedItems;
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
@ -95,61 +116,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();
}
SingleSelect = !SelectionMode.HasFlagCustom(SelectionMode.Multiple),
AutoSelect = SelectionMode.HasFlagCustom(SelectionMode.AlwaysSelected),
RetainSelectionOnReset = true,
};
return _selectedItems;
}
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();
}
UnsubscribeFromSelectedItems();
_selectedItems = value ?? new AvaloniaList<object>();
SubscribeToSelectedItems();
_selection = value;
if (_selection != null)
{
_selection.Source = Items;
_selection.PropertyChanged += OnSelectionModelPropertyChanged;
_selection.SelectionChanged += OnSelectionModelSelectionChanged;
_selection.ChildrenRequested += OnSelectionModelChildrenRequested;
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 +244,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 +333,79 @@ 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;
if (container is object)
{
e.Children = Observable.CombineLatest(
container.GetObservable(TreeViewItem.IsExpandedProperty),
container.GetObservable(ItemsProperty),
(expanded, items) => expanded ? items : null);
}
}
private TreeViewItem GetContainerInDirection(
TreeViewItem from,
NavigationDirection direction,
@ -498,6 +459,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 +486,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 +513,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 +532,48 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
if (rightButton)
if (!select)
{
if (!SelectedItems.Contains(item))
Selection.DeselectAt(index);
}
else if (rightButton)
{
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))
{
Selection.DeselectAt(index);
}
else if (multi)
{
SelectedItems.Remove(item);
Selection.SelectAt(index);
}
else
{
if (multi)
{
SelectedItems.Add(item);
}
else
{
SelectedItem = item;
}
Selection.SelectedIndex = index;
}
}
}
@ -610,117 +596,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 +701,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);
}
}
foreach (var i in toRemove)
private void UpdateContainerSelection()
{
var index = ItemContainerGenerator.Index;
foreach (var container in index.Containers)
{
items.Remove(i);
var i = IndexFromContainer((TreeViewItem)container);
MarkContainerSelected(
container,
Selection.IsSelectedAt(i) != false);
}
}
private static IndexPath IndexFromContainer(TreeViewItem container)
{
var result = new List<int>();
foreach (var i in toAdd)
while (true)
{
items.Add(i);
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;
if (container != null)
{
return IndexFromContainer(container);
}
return default;
}
private TreeViewItem ContainerFromIndex(IndexPath index)
{
TreeViewItem treeViewItem = null;
for (var i = 0; i < index.GetSize(); ++i)
{
var generator = treeViewItem?.ItemContainerGenerator ?? ItemContainerGenerator;
treeViewItem = generator.ContainerFromIndex(index.GetAt(i)) as TreeViewItem;
if (treeViewItem == null)
{
return null;
}
}
return treeViewItem;
}
}
}

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

Loading…
Cancel
Save