diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index dba3ee6d31..85fcf20034 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; }; 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; }; + 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; @@ -51,6 +52,8 @@ 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = ""; }; 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = ""; }; 522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; + 523484C926EA688F00EA0C2C /* trayicon.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = trayicon.mm; sourceTree = ""; }; + 523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = ""; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; @@ -114,6 +117,8 @@ AB00E4F62147CA920032A60A /* main.mm */, 37155CE3233C00EB0034DCE9 /* menu.h */, 520624B222973F4100C4DCEF /* menu.mm */, + 523484C926EA688F00EA0C2C /* trayicon.mm */, + 523484CB26EA68AA00EA0C2C /* trayicon.h */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 37A517B22159597E00FBA241 /* Screens.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */, @@ -204,6 +209,7 @@ 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, + 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index c082003ccf..8896fbe88b 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -22,6 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); +extern IAvnTrayIcon* CreateTrayIcon(); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 3e152a6125..eeaaecfdbd 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -303,6 +303,17 @@ public: } } + virtual HRESULT CreateTrayIcon (IAvnTrayIcon** ppv) override + { + START_COM_CALL; + + @autoreleasepool + { + *ppv = ::CreateTrayIcon(); + return S_OK; + } + } + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { START_COM_CALL; diff --git a/native/Avalonia.Native/src/OSX/trayicon.h b/native/Avalonia.Native/src/OSX/trayicon.h new file mode 100644 index 0000000000..f94f9a871b --- /dev/null +++ b/native/Avalonia.Native/src/OSX/trayicon.h @@ -0,0 +1,33 @@ +// +// trayicon.h +// Avalonia.Native.OSX +// +// Created by Dan Walmsley on 09/09/2021. +// Copyright © 2021 Avalonia. All rights reserved. +// + +#ifndef trayicon_h +#define trayicon_h + +#include "common.h" + +class AvnTrayIcon : public ComSingleObject +{ +private: + NSStatusItem* _native; + +public: + FORWARD_IUNKNOWN() + + AvnTrayIcon(); + + ~AvnTrayIcon (); + + virtual HRESULT SetIcon (void* data, size_t length) override; + + virtual HRESULT SetMenu (IAvnMenu* menu) override; + + virtual HRESULT SetIsVisible (bool isVisible) override; +}; + +#endif /* trayicon_h */ diff --git a/native/Avalonia.Native/src/OSX/trayicon.mm b/native/Avalonia.Native/src/OSX/trayicon.mm new file mode 100644 index 0000000000..151990cfb1 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/trayicon.mm @@ -0,0 +1,85 @@ +#include "common.h" +#include "trayicon.h" +#include "menu.h" + +extern IAvnTrayIcon* CreateTrayIcon() +{ + @autoreleasepool + { + return new AvnTrayIcon(); + } +} + +AvnTrayIcon::AvnTrayIcon() +{ + _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; + +} + +AvnTrayIcon::~AvnTrayIcon() +{ + if(_native != nullptr) + { + [[_native statusBar] removeStatusItem:_native]; + _native = nullptr; + } +} + +HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) +{ + START_COM_CALL; + + @autoreleasepool + { + if(data != nullptr) + { + NSData *imageData = [NSData dataWithBytes:data length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + NSSize originalSize = [image size]; + + NSSize size; + size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; + + auto scaleFactor = size.height / originalSize.height; + size.width = originalSize.width * scaleFactor; + + [image setSize: size]; + [_native setImage:image]; + } + else + { + [_native setImage:nullptr]; + } + return S_OK; + } +} + +HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) +{ + START_COM_CALL; + + @autoreleasepool + { + auto appMenu = dynamic_cast(menu); + + if(appMenu != nullptr) + { + [_native setMenu:appMenu->GetNative()]; + } + } + + return S_OK; +} + +HRESULT AvnTrayIcon::SetIsVisible(bool isVisible) +{ + START_COM_CALL; + + @autoreleasepool + { + [_native setVisible:isVisible]; + } + + return S_OK; +} diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3a54bd4b79..1dc091a48d 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -12,6 +12,7 @@ class WindowBaseImpl; -(AvnPixelSize) getPixelSize; -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; ++ (AvnPoint)toAvnPoint:(CGPoint)p; @end @interface AutoFitContentView : NSView diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7a6e7dc72f..26c065fe11 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -52,7 +52,6 @@ public: [Window setBackingType:NSBackingStoreBuffered]; [Window setOpaque:false]; - [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -125,6 +124,8 @@ public: SetPosition(lastPositionSet); UpdateStyle(); + [Window setContentView: StandardContainer]; + [Window setTitle:_lastTitle]; if(ShouldTakeFocusOnShow() && activate) @@ -231,6 +232,8 @@ public: virtual HRESULT GetFrameSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -321,6 +324,7 @@ public: BaseEvents->Resized(AvnSize{x,y}, reason); } + [StandardContainer setFrameSize:NSSize{x,y}]; [Window setContentSize:NSSize{x, y}]; } @finally @@ -1541,7 +1545,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1598,7 +1602,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } auto localPoint = [self convertPoint:[event locationInWindow] toView:self]; - auto avnPoint = [self toAvnPoint:localPoint]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; AvnVector delta; @@ -1943,7 +1947,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info { auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; - auto avnPoint = [self toAvnPoint:localPoint]; + auto avnPoint = [AvnView toAvnPoint:localPoint]; auto point = [self translateLocalPoint:avnPoint]; auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; NSDragOperation nsop = [info draggingSourceOperationMask]; @@ -2376,6 +2380,48 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->BaseEvents->PositionChanged(position); } } + +- (AvnPoint) translateLocalPoint:(AvnPoint)pt +{ + pt.Y = [self frame].size.height - pt.Y; + return pt; +} + +- (void)sendEvent:(NSEvent *)event +{ + if(_parent != nullptr) + { + switch(event.type) + { + case NSEventTypeLeftMouseDown: + { + auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]]; + auto point = [self translateLocalPoint:avnPoint]; + AvnVector delta; + + _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta); + } + break; + + case NSEventTypeMouseEntered: + { + _parent->UpdateCursor(); + } + break; + + case NSEventTypeMouseExited: + { + [[NSCursor arrowCursor] set]; + } + break; + + default: + break; + } + } + + [super sendEvent:event]; +} @end class PopupImpl : public virtual WindowBaseImpl, public IAvnPopup diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index de3830ffea..3f9ccb04eb 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -54,6 +54,7 @@ + diff --git a/readme.md b/readme.md index 7959bc5540..ef8c116728 100644 --- a/readme.md +++ b/readme.md @@ -60,6 +60,9 @@ See the [build instructions here](Documentation/build.md). ## Contributing +This project exists thanks to all the people who contribute. + + Please read the [contribution guidelines](CONTRIBUTING.md) before submitting a pull request. ## Code of Conduct @@ -71,11 +74,6 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou Avalonia is licenced under the [MIT licence](licence.md). -## Contributors - -This project exists thanks to all the people who contribute. [[Contribute](https://avaloniaui.net/contributing)]. - - ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)] diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 6aad44c0d5..6e57686e00 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -1,5 +1,8 @@ - + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index f3ec7b48aa..36b6fc2dcd 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -1,14 +1,21 @@ using System; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; +using ControlCatalog.ViewModels; namespace ControlCatalog { public class App : Application { + public App() + { + DataContext = new ApplicationViewModel(); + } + private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") @@ -97,7 +104,9 @@ namespace ControlCatalog public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { desktopLifetime.MainWindow = new MainWindow(); + } else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) singleViewLifetime.MainView = new MainView(); diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 6537c470d5..f61b59e6cd 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -98,6 +98,7 @@ Transparent Blur AcrylicBlur + Mica diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a107ee2163..375345f64e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -12,6 +12,7 @@ ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" TransparencyLevelHint="{Binding TransparencyLevel}" x:Name="MainWindow" + Background="Transparent" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> @@ -62,11 +63,11 @@ - - + + - - + + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 2446c0e1c9..a9900471c5 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -35,6 +35,8 @@ namespace ControlCatalog var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; + + ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar; } public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index be114bbbc9..b35c112a68 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index 1359cfa2ef..769ef26699 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -11,9 +11,9 @@ Spacing="16"> - Unchecked - Checked - Indeterminate + _Unchecked + _Checked + _Indeterminate Disabled Use filters - - - - - - - - - - + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 392ccb57c3..4d0bd663df 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,17 +1,33 @@ + + + + + + - - diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index f515db84d4..b36629fb2a 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -2,9 +2,20 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.Pages.ListBoxPage"> + + + + ListBox Hosts a collection of ListBoxItem. + Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules. Multiple diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index bf31c40e2a..408f9c2411 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -11,9 +11,9 @@ Spacing="16"> - Option 1 - Option 2 - Option 3 + _Option 1 + O_ption 2 + Op_tion 3 Disabled - + + + + Custom context flyout + + + - + @@ -24,7 +24,7 @@ - @@ -40,7 +40,7 @@ ContentOff="Off" />" - + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml index b90f43c3b6..caab42e98c 100644 --- a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -14,6 +14,7 @@ Transparent Blur AcrylicBlur + Mica diff --git a/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs new file mode 100644 index 0000000000..7eea7b0657 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/ApplicationViewModel.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using MiniMvvm; + +namespace ControlCatalog.ViewModels +{ + public class ApplicationViewModel : ViewModelBase + { + public ApplicationViewModel() + { + ExitCommand = MiniCommand.Create(() => + { + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Shutdown(); + } + }); + + ToggleCommand = MiniCommand.Create(() => { }); + } + + public MiniCommand ExitCommand { get; } + + public MiniCommand ToggleCommand { get; } + } +} diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 2ad220dddd..c049f9e763 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -39,5 +39,10 @@ namespace Avalonia.Logging /// The log event comes from Win32Platform. /// public const string Win32Platform = nameof(Win32Platform); + + /// + /// The log event comes from X11Platform. + /// + public const string X11Platform = nameof(X11Platform); } } diff --git a/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs new file mode 100644 index 0000000000..de41f5292c --- /dev/null +++ b/src/Avalonia.Base/LogicalTree/ChildIndexChangedEventArgs.cs @@ -0,0 +1,26 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + /// + /// Event args for event. + /// + public class ChildIndexChangedEventArgs : EventArgs + { + public ChildIndexChangedEventArgs() + { + } + + public ChildIndexChangedEventArgs(ILogical child) + { + Child = child; + } + + /// + /// Logical child which index was changed. + /// If null, all children should be reset. + /// + public ILogical? Child { get; } + } +} diff --git a/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs new file mode 100644 index 0000000000..7fcd73273c --- /dev/null +++ b/src/Avalonia.Base/LogicalTree/IChildIndexProvider.cs @@ -0,0 +1,32 @@ +#nullable enable +using System; + +namespace Avalonia.LogicalTree +{ + /// + /// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.) + /// + /// + /// Used by nth-child and nth-last-child selectors. + /// + public interface IChildIndexProvider + { + /// + /// Gets child's actual index in order of the original source. + /// + /// Logical child. + /// Index or -1 if child was not found. + int GetChildIndex(ILogical child); + + /// + /// Total children count or null if source is infinite. + /// Some Avalonia features might not work if returns false, for instance: nth-last-child selector. + /// + bool TryGetTotalCount(out int count); + + /// + /// Notifies subscriber when child's index or total count was changed. + /// + event EventHandler? ChildIndexChanged; + } +} diff --git a/src/Avalonia.Base/Media/DrawingImage.cs b/src/Avalonia.Base/Media/DrawingImage.cs index 67873adbfa..7d504a7cc9 100644 --- a/src/Avalonia.Base/Media/DrawingImage.cs +++ b/src/Avalonia.Base/Media/DrawingImage.cs @@ -9,6 +9,14 @@ namespace Avalonia.Media /// public class DrawingImage : AvaloniaObject, IImage, IAffectsRender { + public DrawingImage() + { + } + + public DrawingImage(Drawing drawing) + { + Drawing = drawing; + } /// /// Defines the property. /// diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index fcd7d69e7b..7f9e878419 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -8,5 +8,6 @@ namespace Avalonia.Metadata [AttributeUsage(AttributeTargets.Property)] public class TemplateContentAttribute : Attribute { + public Type TemplateResultType { get; set; } } } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index 6c84cfd55c..fe63fdec46 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -279,13 +279,13 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; + return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty; } /// void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) { - var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; + var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]; if (childScene != null) { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index ada04bfefd..90430bed02 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -11,19 +11,23 @@ namespace Avalonia.Rendering.SceneGraph /// Initializes a new instance of the class that represents a /// clip push. /// + /// The current transform. /// The clip to push. - public ClipNode(Rect clip) + public ClipNode(Matrix transform, Rect clip) { + Transform = transform; Clip = clip; } - + /// /// Initializes a new instance of the class that represents a /// clip push. /// + /// The current transform. /// The clip to push. - public ClipNode(RoundedRect clip) + public ClipNode(Matrix transform, RoundedRect clip) { + Transform = transform; Clip = clip; } @@ -43,23 +47,31 @@ namespace Avalonia.Rendering.SceneGraph /// public RoundedRect? Clip { get; } + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + /// public bool HitTest(Point p) => false; /// /// Determines if this draw operation equals another. /// + /// The transform of the other draw operation. /// The clip of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(RoundedRect? clip) => Clip == clip; + public bool Equals(Matrix transform, RoundedRect? clip) => Transform == transform && Clip == clip; /// public void Render(IDrawingContextImpl context) { + context.Transform = Transform; + if (Clip.HasValue) { context.PushClip(Clip.Value); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 1f89f6c0c2..cc3cd6f9e4 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -303,9 +303,9 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(clip)) + if (next == null || !next.Item.Equals(Transform, clip)) { - Add(new ClipNode(clip)); + Add(new ClipNode(Transform, clip)); } else { @@ -318,9 +318,9 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(clip)) + if (next == null || !next.Item.Equals(Transform, clip)) { - Add(new ClipNode(clip)); + Add(new ClipNode(Transform, clip)); } else { @@ -333,9 +333,9 @@ namespace Avalonia.Rendering.SceneGraph { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(clip)) + if (next == null || !next.Item.Equals(Transform, clip)) { - Add(new GeometryClipNode(clip)); + Add(new GeometryClipNode(Transform, clip)); } else { diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index a2ad83d845..16092d4cbb 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -11,9 +11,11 @@ namespace Avalonia.Rendering.SceneGraph /// Initializes a new instance of the class that represents a /// geometry clip push. /// + /// The current transform. /// The clip to push. - public GeometryClipNode(IGeometryImpl clip) + public GeometryClipNode(Matrix transform, IGeometryImpl clip) { + Transform = transform; Clip = clip; } @@ -33,23 +35,31 @@ namespace Avalonia.Rendering.SceneGraph /// public IGeometryImpl Clip { get; } + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + /// public bool HitTest(Point p) => false; /// /// Determines if this draw operation equals another. /// + /// The transform of the other draw operation. /// The clip of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(IGeometryImpl clip) => Clip == clip; + public bool Equals(Matrix transform, IGeometryImpl clip) => Transform == transform && Clip == clip; /// public void Render(IDrawingContextImpl context) { + context.Transform = Transform; + if (Clip != null) { context.PushGeometryClip(Clip); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs index 54a9ff733d..3dc6d5f50e 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; @@ -82,8 +83,46 @@ namespace Avalonia.Rendering.SceneGraph public override bool HitTest(Point p) { - // TODO: Implement line hit testing. - return false; + if (!Transform.HasInverse) + return false; + + p *= Transform.Invert(); + + var halfThickness = Pen.Thickness / 2; + var minX = Math.Min(P1.X, P2.X) - halfThickness; + var maxX = Math.Max(P1.X, P2.X) + halfThickness; + var minY = Math.Min(P1.Y, P2.Y) - halfThickness; + var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; + + if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) + return false; + + var a = P1; + var b = P2; + + //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. + //The distance from a point to a straight line is defined as the + //length of the vector formed by the point and the closest point of the segment + + Vector ap = p - a; + var dot1 = Vector.Dot(b - a, ap); + + if (dot1 < 0) + return ap.Length <= Pen.Thickness / 2; + + Vector bp = p - b; + var dot2 = Vector.Dot(a - b, bp); + + if (dot2 < 0) + return bp.Length <= halfThickness; + + var bXaX = b.X - a.X; + var bYaY = b.Y - a.Y; + + var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / + (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); + + return Math.Abs(distance) <= halfThickness; } } } diff --git a/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs new file mode 100644 index 0000000000..803809a8ce --- /dev/null +++ b/src/Avalonia.Base/Styling/Activators/NthChildActivator.cs @@ -0,0 +1,56 @@ +#nullable enable +using Avalonia.LogicalTree; + +namespace Avalonia.Styling.Activators +{ + /// + /// An which is active when control's index was changed. + /// + internal sealed class NthChildActivator : StyleActivatorBase + { + private readonly ILogical _control; + private readonly IChildIndexProvider _provider; + private readonly int _step; + private readonly int _offset; + private readonly bool _reversed; + + public NthChildActivator( + ILogical control, + IChildIndexProvider provider, + int step, int offset, bool reversed) + { + _control = control; + _provider = provider; + _step = step; + _offset = offset; + _reversed = reversed; + } + + protected override void Initialize() + { + PublishNext(IsMatching()); + _provider.ChildIndexChanged += ChildIndexChanged; + } + + protected override void Deinitialize() + { + _provider.ChildIndexChanged -= ChildIndexChanged; + } + + private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e) + { + // Run matching again if: + // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index. + // 2. e.Child is null, when all children indeces were changed. + // 3. Subscribed child index was changed. + if (_reversed + || e.Child is null + || e.Child == _control) + { + PublishNext(IsMatching()); + } + } + + private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch; + } +} diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs new file mode 100644 index 0000000000..aff34ea17c --- /dev/null +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -0,0 +1,145 @@ +#nullable enable +using System; +using System.Text; +using Avalonia.LogicalTree; +using Avalonia.Styling.Activators; + +namespace Avalonia.Styling +{ + /// + /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings. + /// + /// + /// Element indices are 1-based. + /// + public class NthChildSelector : Selector + { + private const string NthChildSelectorName = "nth-child"; + private const string NthLastChildSelectorName = "nth-last-child"; + private readonly Selector? _previous; + private readonly bool _reversed; + + internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed) + { + _previous = previous; + Step = step; + Offset = offset; + _reversed = reversed; + } + + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset. + public NthChildSelector(Selector? previous, int step, int offset) + : this(previous, step, offset, false) + { + + } + + public override bool InTemplate => _previous?.InTemplate ?? false; + + public override bool IsCombinator => false; + + public override Type? TargetType => _previous?.TargetType; + + public int Step { get; } + public int Offset { get; } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + if (!(control is ILogical logical)) + { + return SelectorMatch.NeverThisType; + } + + var controlParent = logical.LogicalParent; + + if (controlParent is IChildIndexProvider childIndexProvider) + { + return subscribe + ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed)) + : Evaluate(logical, childIndexProvider, Step, Offset, _reversed); + } + else + { + return SelectorMatch.NeverThisInstance; + } + } + + internal static SelectorMatch Evaluate( + ILogical logical, IChildIndexProvider childIndexProvider, + int step, int offset, bool reversed) + { + var index = childIndexProvider.GetChildIndex(logical); + if (index < 0) + { + return SelectorMatch.NeverThisInstance; + } + + if (reversed) + { + if (childIndexProvider.TryGetTotalCount(out var totalCountValue)) + { + index = totalCountValue - index; + } + else + { + return SelectorMatch.NeverThisInstance; + } + } + else + { + // nth child index is 1-based + index += 1; + } + + var n = Math.Sign(step); + + var diff = index - offset; + var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0); + + return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance; + } + + protected override Selector? MovePrevious() => _previous; + + public override string ToString() + { + var expectedCapacity = NthLastChildSelectorName.Length + 8; + var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity); + + stringBuilder.Append(':'); + stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); + stringBuilder.Append('('); + + var hasStep = false; + if (Step != 0) + { + hasStep = true; + stringBuilder.Append(Step); + stringBuilder.Append('n'); + } + + if (Offset > 0) + { + if (hasStep) + { + stringBuilder.Append('+'); + } + stringBuilder.Append(Offset); + } + else if (Offset < 0) + { + stringBuilder.Append('-'); + stringBuilder.Append(-Offset); + } + + stringBuilder.Append(')'); + + return stringBuilder.ToString(); + } + } +} diff --git a/src/Avalonia.Base/Styling/NthLastChildSelector.cs b/src/Avalonia.Base/Styling/NthLastChildSelector.cs new file mode 100644 index 0000000000..6f6abbae6a --- /dev/null +++ b/src/Avalonia.Base/Styling/NthLastChildSelector.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace Avalonia.Styling +{ + /// + /// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end. + /// + /// + /// Element indices are 1-based. + /// + public class NthLastChildSelector : NthChildSelector + { + /// + /// Creates an instance of + /// + /// Previous selector. + /// Position step. + /// Initial index offset, counting from the end. + public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true) + { + } + } +} diff --git a/src/Avalonia.Base/Styling/Selectors.cs b/src/Avalonia.Base/Styling/Selectors.cs index 762ed7b58c..64d0a0e96b 100644 --- a/src/Avalonia.Base/Styling/Selectors.cs +++ b/src/Avalonia.Base/Styling/Selectors.cs @@ -123,6 +123,22 @@ namespace Avalonia.Styling return new NotSelector(previous, argument); } + /// + /// + /// The selector. + public static Selector NthChild(this Selector previous, int step, int offset) + { + return new NthChildSelector(previous, step, offset); + } + + /// + /// + /// The selector. + public static Selector NthLastChild(this Selector previous, int step, int offset) + { + return new NthLastChildSelector(previous, step, offset); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index fea02dabf4..10c7c16488 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2223,6 +2223,7 @@ namespace Avalonia.Controls if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0) { var handled = false; + var ignoreInvalidate = false; var scrollHeight = 0d; // Vertical scroll handling @@ -2252,8 +2253,7 @@ namespace Avalonia.Controls // Horizontal scroll handling if (delta.X != 0) { - var originalHorizontalOffset = HorizontalOffset; - var horizontalOffset = originalHorizontalOffset - delta.X; + var horizontalOffset = HorizontalOffset - delta.X; var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth); if (horizontalOffset < 0) @@ -2265,16 +2265,20 @@ namespace Avalonia.Controls horizontalOffset = widthNotVisible; } - if (horizontalOffset != originalHorizontalOffset) + if (UpdateHorizontalOffset(horizontalOffset)) { - HorizontalOffset = horizontalOffset; + // We don't need to invalidate once again after UpdateHorizontalOffset. + ignoreInvalidate = true; handled = true; } } if (handled) { - InvalidateRowsMeasure(invalidateIndividualElements: false); + if (!ignoreInvalidate) + { + InvalidateRowsMeasure(invalidateIndividualElements: false); + } return true; } } @@ -2932,7 +2936,7 @@ namespace Avalonia.Controls return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true); } - internal void UpdateHorizontalOffset(double newValue) + internal bool UpdateHorizontalOffset(double newValue) { if (HorizontalOffset != newValue) { @@ -2940,7 +2944,9 @@ namespace Avalonia.Controls InvalidateColumnHeadersMeasure(); InvalidateRowsMeasure(true); + return true; } + return false; } internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView) diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 90401a00a2..97e247bdc6 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -133,7 +133,7 @@ namespace Avalonia.Controls protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem); - internal AvaloniaProperty BindingTarget { get; set; } + protected AvaloniaProperty BindingTarget { get; set; } internal void SetHeaderFromBinding() { diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs index 4ab2869138..5499257171 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs @@ -9,6 +9,7 @@ using Avalonia.VisualTree; using Avalonia.Collections; using Avalonia.Utilities; using System; +using System.ComponentModel; using System.Linq; using System.Diagnostics; using Avalonia.Controls.Utils; @@ -653,6 +654,34 @@ namespace Avalonia.Controls return null; } + /// + /// Clears the current sort direction + /// + public void ClearSort() + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control); + } + + /// + /// Switches the current state of sort direction + /// + public void Sort() + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None); + } + + /// + /// Changes the sort direction of this column + /// + /// New sort direction + public void Sort(ListSortDirection direction) + { + //InvokeProcessSort is already validating if sorting is possible + _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction); + } + /// /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value. /// diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6f957497cb..915b36687c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -35,6 +35,7 @@ namespace Avalonia.Controls private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; + private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5; private bool _areHandlersSuspended; private static DragMode _dragMode; @@ -201,21 +202,21 @@ namespace Avalonia.Controls handled = true; } - internal void InvokeProcessSort(KeyModifiers keyModifiers) + internal void InvokeProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null) { Debug.Assert(OwningGrid != null); - if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers))) + if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers, forcedDirection))) { return; } if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true)) { - Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers)); + Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers, forcedDirection)); } } //TODO GroupSorting - internal void ProcessSort(KeyModifiers keyModifiers) + internal void ProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null) { // if we can sort: // - AllowUserToSortColumns and CanSort are true, and @@ -259,7 +260,14 @@ namespace Avalonia.Controls { if (sort != null) { - newSort = sort.SwitchSortDirection(); + if (forcedDirection == null || sort.Direction != forcedDirection) + { + newSort = sort.SwitchSortDirection(); + } + else + { + newSort = sort; + } // changing direction should not affect sort order, so we replace this column's // sort description instead of just adding it to the end of the collection @@ -276,7 +284,10 @@ namespace Avalonia.Controls } else if (OwningColumn.CustomSortComparer != null) { - newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + newSort = forcedDirection != null ? + DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer, forcedDirection.Value) : + DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer); + owningGrid.DataConnection.SortDescriptions.Add(newSort); } @@ -290,6 +301,10 @@ namespace Avalonia.Controls } newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture); + if (forcedDirection != null && newSort.Direction != forcedDirection) + { + newSort = newSort.SwitchSortDirection(); + } owningGrid.DataConnection.SortDescriptions.Add(newSort); } @@ -434,19 +449,6 @@ namespace Avalonia.Controls OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight); - // if we still haven't done anything about moving the mouse while - // the button is down, we remember that we're dragging, but we don't - // claim to have actually handled the event - if (_dragMode == DragMode.MouseDown) - { - _dragMode = DragMode.Drag; - } - - _lastMousePositionHeaders = mousePositionHeaders; - - if (args.Pointer.Captured != this && _dragMode == DragMode.Drag) - args.Pointer.Capture(this); - SetDragCursor(mousePosition); } @@ -718,15 +720,19 @@ namespace Avalonia.Controls { return; } - + //handle entry into reorder mode - if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) + if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth)) { - handled = CanReorderColumn(OwningColumn); - - if (handled) + var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders); + if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold) { - OnMouseMove_BeginReorder(mousePosition); + handled = CanReorderColumn(OwningColumn); + + if (handled) + { + OnMouseMove_BeginReorder(mousePosition); + } } } diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index fac5923db5..dd41c30e85 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -37,6 +37,7 @@ MembersMustExist : Member 'public System.Action Avalonia.Controls MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. @@ -55,4 +56,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. -Total Issues: 56 +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. +Total Issues: 58 diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index c8337df99c..0c6949465b 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -8,7 +8,9 @@ namespace Avalonia.Controls /// /// A control which decorates a child with a border and background. /// +#pragma warning disable CS0618 // Type or member is obsolete public partial class Border : Decorator, IVisualWithRoundRectClip +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Defines the property. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 6912b2db63..8b22cdd4ec 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -99,6 +99,7 @@ namespace Avalonia.Controls CommandParameterProperty.Changed.Subscribe(CommandParameterChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); IsCancelProperty.Changed.Subscribe(IsCancelChanged); + AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener + public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider { /// /// The default value for the property. @@ -56,6 +56,7 @@ namespace Avalonia.Controls private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -145,11 +146,28 @@ namespace Avalonia.Controls protected set; } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { + if (Presenter is IChildIndexProvider oldInnerProvider) + { + oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged; + } + Presenter = presenter; ItemContainerGenerator.Clear(); + + if (Presenter is IChildIndexProvider innerProvider) + { + innerProvider.ChildIndexChanged += PresenterChildIndexChanged; + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); + } } void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) @@ -506,5 +524,28 @@ namespace Avalonia.Controls return null; } + + private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e) + { + _childIndexChanged?.Invoke(this, e); + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return Presenter is IChildIndexProvider innerProvider + ? innerProvider.GetChildIndex(child) : -1; + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + if (Presenter is IChildIndexProvider presenter + && presenter.TryGetTotalCount(out count)) + { + return true; + } + + count = ItemCount; + return true; + } } } diff --git a/src/Avalonia.Controls/NativeMenu.Export.cs b/src/Avalonia.Controls/NativeMenu.Export.cs index 0349df842b..6bfe5ebc82 100644 --- a/src/Avalonia.Controls/NativeMenu.Export.cs +++ b/src/Avalonia.Controls/NativeMenu.Export.cs @@ -52,15 +52,10 @@ namespace Avalonia.Controls } public static readonly AttachedProperty MenuProperty - = AvaloniaProperty.RegisterAttached("Menu"/*, validate: - (o, v) => - { - if(!(o is Application || o is TopLevel)) - throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType()); - return v; - }*/); + = AvaloniaProperty.RegisterAttached("Menu"); public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); + public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); static NativeMenu() @@ -79,6 +74,10 @@ namespace Avalonia.Controls { GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); } + else if(args.Sender is INativeMenuExporterProvider provider) + { + provider.NativeMenuExporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); + } }); } } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2eaf24d2f2..2ceaeb6dba 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -16,6 +16,7 @@ namespace Avalonia.Controls private bool _isChecked = false; private NativeMenuItemToggleType _toggleType; private IBitmap _icon; + private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; private NativeMenu _menu; @@ -47,8 +48,6 @@ namespace Avalonia.Controls } } - private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; - public NativeMenuItem() { diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 3a83d9ed7c..eba381e5fa 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -332,7 +332,9 @@ namespace Avalonia.Controls /// static NumericUpDown() { +#pragma warning disable CS0612 // Type or member is obsolete CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); +#pragma warning restore CS0612 // Type or member is obsolete NumberFormatProperty.Changed.Subscribe(OnNumberFormatChanged); FormatStringProperty.Changed.Subscribe(FormatStringChanged); IncrementProperty.Changed.Subscribe(IncrementChanged); diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index b7eeb065da..b182f9d261 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -14,7 +16,7 @@ namespace Avalonia.Controls /// Controls can be added to a by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IPanel + public class Panel : Control, IPanel, IChildIndexProvider { /// /// Defines the property. @@ -30,6 +32,8 @@ namespace Avalonia.Controls AffectsRender(BackgroundProperty); } + private EventHandler _childIndexChanged; + /// /// Initializes a new instance of the class. /// @@ -53,6 +57,12 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// /// Renders the visual to a . /// @@ -137,6 +147,7 @@ namespace Avalonia.Controls throw new NotSupportedException(); } + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); InvalidateMeasureOnChildrenChanged(); } @@ -160,5 +171,16 @@ namespace Avalonia.Controls var panel = control?.VisualParent as TPanel; panel?.InvalidateMeasure(); } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is IControl control ? Children.IndexOf(control) : -1; + } + + public bool TryGetTotalCount(out int count) + { + count = Children.Count; + return true; + } } } diff --git a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs index 3ac5f28956..9b779054f3 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs @@ -1,14 +1,25 @@ using System; -using System.Collections.Generic; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Controls.Platform { - public interface ITopLevelNativeMenuExporter + public interface INativeMenuExporter + { + void SetNativeMenu(NativeMenu? menu); + } + + public interface ITopLevelNativeMenuExporter : INativeMenuExporter { bool IsNativeMenuExported { get; } + event EventHandler OnIsNativeMenuExportedChanged; - void SetNativeMenu(NativeMenu menu); + } + + public interface INativeMenuExporterProvider + { + INativeMenuExporter? NativeMenuExporter { get; } } public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl diff --git a/src/Avalonia.Controls/Platform/ITrayIconImpl.cs b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs new file mode 100644 index 0000000000..9768d149f0 --- /dev/null +++ b/src/Avalonia.Controls/Platform/ITrayIconImpl.cs @@ -0,0 +1,35 @@ +using System; +using Avalonia.Controls.Platform; + +#nullable enable + +namespace Avalonia.Platform +{ + public interface ITrayIconImpl : IDisposable + { + /// + /// Sets the icon of this tray icon. + /// + void SetIcon(IWindowIconImpl? icon); + + /// + /// Sets the icon of this tray icon. + /// + void SetToolTipText(string? text); + + /// + /// Sets if the tray icon is visible or not. + /// + void SetIsVisible(bool visible); + + /// + /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon. + /// + INativeMenuExporter? MenuExporter { get; } + + /// + /// Gets or Sets the Action that is called when the TrayIcon is clicked. + /// + Action? OnClicked { get; set; } + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index be8939e19a..21882b1271 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -1,8 +1,13 @@ +#nullable enable + namespace Avalonia.Platform { public interface IWindowingPlatform { IWindowImpl CreateWindow(); + IWindowImpl CreateEmbeddableWindow(); + + ITrayIconImpl? CreateTrayIcon(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index 19d034b4e2..e39f0b1e99 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -1,8 +1,9 @@ using System; using System.Reactive.Disposables; -using Avalonia.Media; using Avalonia.Platform; +#nullable enable + namespace Avalonia.Controls.Platform { public static partial class PlatformManager @@ -22,6 +23,19 @@ namespace Avalonia.Controls.Platform { } + public static ITrayIconImpl? CreateTrayIcon() + { + var platform = AvaloniaLocator.Current.GetService(); + + if (platform == null) + { + throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered."); + } + + return s_designerMode ? null : platform.CreateTrayIcon(); + } + + public static IWindowImpl CreateWindow() { var platform = AvaloniaLocator.Current.GetService(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 52f173fc71..aeead7bfd0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -5,6 +5,7 @@ using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.LogicalTree; using Avalonia.Styling; namespace Avalonia.Controls.Presenters @@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Base class for controls that present items inside an . /// - public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl + public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider { /// /// Defines the property. @@ -36,6 +37,7 @@ namespace Avalonia.Controls.Presenters private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; + private EventHandler _childIndexChanged; /// /// Initializes static members of the class. @@ -129,6 +131,12 @@ namespace Avalonia.Controls.Presenters protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + /// public override sealed void ApplyTemplate() { @@ -149,6 +157,8 @@ namespace Avalonia.Controls.Presenters if (Panel != null) { ItemsChanged(e); + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs()); } } @@ -169,9 +179,21 @@ namespace Avalonia.Controls.Presenters result.ItemTemplate = ItemTemplate; } + result.Materialized += ContainerActionHandler; + result.Dematerialized += ContainerActionHandler; + result.Recycled += ContainerActionHandler; + return result; } + private void ContainerActionHandler(object sender, ItemContainerEventArgs e) + { + for (var i = 0; i < e.Containers.Count; i++) + { + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl)); + } + } + /// protected override Size MeasureOverride(Size availableSize) { @@ -248,5 +270,22 @@ namespace Avalonia.Controls.Presenters { (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + if (child is IControl control && ItemContainerGenerator is { } generator) + { + var index = ItemContainerGenerator.IndexFromContainer(control); + + return index; + } + + return -1; + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + return Items.TryGetCountFast(out count); + } } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index b0b52812b9..a62ba306ab 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -60,9 +60,6 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); - // Arbitrary chosen value, probably need to ask ILogicalScrollable - private const int LogicalScrollItemSize = 50; - private bool _canHorizontallyScroll; private bool _canVerticallyScroll; private bool _arranging; @@ -351,7 +348,8 @@ namespace Avalonia.Controls.Presenters if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) { var scrollable = Child as ILogicalScrollable; - bool isLogical = scrollable?.IsLogicalScrollEnabled == true; + var isLogical = scrollable?.IsLogicalScrollEnabled == true; + var logicalScrollItemSize = new Vector(1, 1); double x = Offset.X; double y = Offset.Y; @@ -361,13 +359,18 @@ namespace Avalonia.Controls.Presenters _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta); delta += e.Delta; + if (isLogical && scrollable is object) + { + logicalScrollItemSize = Bounds.Size / scrollable.Viewport; + } + if (Extent.Height > Viewport.Height) { double dy; if (isLogical) { - var logicalUnits = delta.Y / LogicalScrollItemSize; - delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); + var logicalUnits = delta.Y / logicalScrollItemSize.Y; + delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y); dy = logicalUnits * scrollable!.ScrollSize.Height; } else @@ -384,8 +387,8 @@ namespace Avalonia.Controls.Presenters double dx; if (isLogical) { - var logicalUnits = delta.X / LogicalScrollItemSize; - delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); + var logicalUnits = delta.X / logicalScrollItemSize.X; + delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X); dx = logicalUnits * scrollable!.ScrollSize.Width; } else diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index c42c6f100c..3c82386991 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -68,7 +68,7 @@ namespace Avalonia.Controls.Primitives if (underscore != -1 && ShowAccessKey) { var rect = TextLayout.HitTestTextPosition(underscore); - var offset = new Vector(0, -0.5); + var offset = new Vector(0, -1.5); context.DrawLine( new Pen(Foreground, 1), rect.BottomLeft + offset, diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index a5cdeefb0e..4c9b95c22d 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -20,10 +20,12 @@ namespace Avalonia.Controls.Primitives /// /// Displays a popup window. /// +#pragma warning disable CS0612 // Type or member is obsolete public class Popup : Control, IVisualTreeHost, IPopupHostProvider +#pragma warning restore CS0612 // Type or member is obsolete { public static readonly StyledProperty WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 0f68f5d258..844069965b 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -447,8 +447,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect) { // We need a better way for tracking the last pointer position +#pragma warning disable CS0618 // Type or member is obsolete var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position); - +#pragma warning restore CS0618 // Type or member is obsolete + positionerParameters.Offset = offset; positionerParameters.ConstraintAdjustment = constraintAdjustment; if (placement == PlacementMode.Pointer) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 9ab5a73af0..a848901bc3 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -218,9 +218,12 @@ namespace Avalonia.Controls TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150% TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166% + +#pragma warning disable CS0618 // Type or member is obsolete // Remove these properties when we switch to fluent as default and removed the old one. IndeterminateStartingOffset = -dim; IndeterminateEndingOffset = dim; +#pragma warning restore CS0618 // Type or member is obsolete var padding = Padding; var rectangle = new RectangleGeometry( diff --git a/src/Avalonia.Controls/Remote/RemoteWidget.cs b/src/Avalonia.Controls/Remote/RemoteWidget.cs index 234960e87c..b839a8769a 100644 --- a/src/Avalonia.Controls/Remote/RemoteWidget.cs +++ b/src/Avalonia.Controls/Remote/RemoteWidget.cs @@ -77,8 +77,10 @@ namespace Avalonia.Controls.Remote _bitmap.PixelSize.Height != _lastFrame.Height) { _bitmap?.Dispose(); +#pragma warning disable CS0618 // Type or member is obsolete _bitmap = new WriteableBitmap(new PixelSize(_lastFrame.Width, _lastFrame.Height), new Vector(96, 96), fmt); +#pragma warning restore CS0618 // Type or member is obsolete } using (var l = _bitmap.Lock()) { diff --git a/src/Avalonia.Controls/RepeatButton.cs b/src/Avalonia.Controls/RepeatButton.cs index ba770634d9..a21725cadf 100644 --- a/src/Avalonia.Controls/RepeatButton.cs +++ b/src/Avalonia.Controls/RepeatButton.cs @@ -70,6 +70,16 @@ namespace Avalonia.Controls _repeatTimer?.Stop(); } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault() == false) + { + StopTimer(); + } + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 01200e87e3..ecc0fa3a48 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.LogicalTree; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -19,7 +20,7 @@ namespace Avalonia.Controls /// Represents a data-driven collection control that incorporates a flexible layout system, /// custom views, and virtualization. /// - public class ItemsRepeater : Panel + public class ItemsRepeater : Panel, IChildIndexProvider { /// /// Defines the property. @@ -61,8 +62,9 @@ namespace Avalonia.Controls private readonly ViewportManager _viewportManager; private IEnumerable _items; private VirtualizingLayoutContext _layoutContext; - private NotifyCollectionChangedEventArgs _processingItemsSourceChange; + private EventHandler _childIndexChanged; private bool _isLayoutInProgress; + private NotifyCollectionChangedEventArgs _processingItemsSourceChange; private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs; private ItemsRepeaterElementClearingEventArgs _elementClearingArgs; private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs; @@ -163,6 +165,25 @@ namespace Avalonia.Controls } } + event EventHandler IChildIndexProvider.ChildIndexChanged + { + add => _childIndexChanged += value; + remove => _childIndexChanged -= value; + } + + int IChildIndexProvider.GetChildIndex(ILogical child) + { + return child is IControl control + ? GetElementIndex(control) + : -1; + } + + bool IChildIndexProvider.TryGetTotalCount(out int count) + { + count = ItemsSourceView.Count; + return true; + } + /// /// Occurs each time an element is cleared and made available to be re-used. /// @@ -545,6 +566,8 @@ namespace Avalonia.Controls ElementPrepared(this, _elementPreparedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementClearing(IControl element) @@ -562,6 +585,8 @@ namespace Avalonia.Controls ElementClearing(this, _elementClearingArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) @@ -579,6 +604,8 @@ namespace Avalonia.Controls ElementIndexChanged(this, _elementIndexChangedArgs); } + + _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element)); } private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue) @@ -588,14 +615,14 @@ namespace Avalonia.Controls throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout."); } - ItemsSourceView?.Dispose(); - ItemsSourceView = newValue; - if (oldValue != null) { oldValue.CollectionChanged -= OnItemsSourceViewChanged; } + ItemsSourceView?.Dispose(); + ItemsSourceView = newValue; + if (newValue != null) { newValue.CollectionChanged += OnItemsSourceViewChanged; diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 7414f438a1..ab46884402 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Controls.Primitives; using Avalonia.Styling; @@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates { } - public class ControlTemplateResult + public class ControlTemplateResult : TemplateResult { public IControl Control { get; } - public INameScope NameScope { get; } - public ControlTemplateResult(IControl control, INameScope nameScope) + public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope) { Control = control; - NameScope = nameScope; } - public void Deconstruct(out IControl control, out INameScope scope) + public new void Deconstruct(out IControl control, out INameScope scope) { control = Control; scope = NameScope; diff --git a/src/Avalonia.Controls/Templates/TemplateResult.cs b/src/Avalonia.Controls/Templates/TemplateResult.cs new file mode 100644 index 0000000000..770aecc329 --- /dev/null +++ b/src/Avalonia.Controls/Templates/TemplateResult.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Controls.Templates +{ + public class TemplateResult + { + public T Result { get; } + public INameScope NameScope { get; } + + public TemplateResult(T result, INameScope nameScope) + { + Result = result; + NameScope = nameScope; + } + + public void Deconstruct(out T result, out INameScope scope) + { + result = Result; + scope = NameScope; + } + } +} diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9eae928eeb..0a29db555c 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -991,7 +991,9 @@ namespace Avalonia.Controls { var point = e.GetPosition(_presenter); var index = CaretIndex = _presenter.GetCaretIndex(point); +#pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) +#pragma warning restore CS0618 // Type or member is obsolete { case 1: SelectionStart = SelectionEnd = index; diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs new file mode 100644 index 0000000000..59edb6278a --- /dev/null +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; +using Avalonia.Collections; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Utilities; + +#nullable enable + +namespace Avalonia.Controls +{ + public sealed class TrayIcons : AvaloniaList + { + } + + + + public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable + { + private readonly ITrayIconImpl? _impl; + private ICommand? _command; + + private TrayIcon(ITrayIconImpl? impl) + { + if (impl != null) + { + _impl = impl; + + _impl.SetIsVisible(IsVisible); + + _impl.OnClicked = () => + { + Clicked?.Invoke(this, EventArgs.Empty); + + if (Command?.CanExecute(CommandParameter) == true) + { + Command.Execute(CommandParameter); + } + }; + } + } + + public TrayIcon() : this(PlatformManager.CreateTrayIcon()) + { + } + + static TrayIcon() + { + IconsProperty.Changed.Subscribe(args => + { + if (args.Sender is Application) + { + if (args.OldValue.Value != null) + { + RemoveIcons(args.OldValue.Value); + } + + if (args.NewValue.Value != null) + { + args.NewValue.Value.CollectionChanged += Icons_CollectionChanged; + } + } + }); + + if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + lifetime.Exit += Lifetime_Exit; + } + } + + /// + /// Raised when the TrayIcon is clicked. + /// Note, this is only supported on Win32 and some Linux DEs, + /// on OSX this event is not raised. + /// + public event EventHandler? Clicked; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + trayIcon => trayIcon.Command, + (trayIcon, command) => trayIcon.Command = command, + enableDataValidation: true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); + + /// + /// Defines the attached property. + /// + public static readonly AttachedProperty IconsProperty + = AvaloniaProperty.RegisterAttached("Icons"); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MenuProperty + = AvaloniaProperty.Register(nameof(Menu)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IconProperty = + Window.IconProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ToolTipTextProperty = + AvaloniaProperty.Register(nameof(ToolTipText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsVisibleProperty = + Visual.IsVisibleProperty.AddOwner(); + + public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); + + public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); + + /// + /// Gets or sets the property of a TrayIcon. + /// + public ICommand? Command + { + get => _command; + set => SetAndRaise(CommandProperty, ref _command, value); + } + + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } + + /// + /// Gets or sets the Menu of the TrayIcon. + /// + public NativeMenu? Menu + { + get => GetValue(MenuProperty); + set => SetValue(MenuProperty, value); + } + + /// + /// Gets or sets the icon of the TrayIcon. + /// + public WindowIcon Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Gets or sets the tooltip text of the TrayIcon. + /// + public string? ToolTipText + { + get => GetValue(ToolTipTextProperty); + set => SetValue(ToolTipTextProperty, value); + } + + /// + /// Gets or sets the visibility of the TrayIcon. + /// + public bool IsVisible + { + get => GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter; + + private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) + { + var trayIcons = GetIcons(Application.Current); + + RemoveIcons(trayIcons); + } + + private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + RemoveIcons(e.OldItems.Cast()); + } + + private static void RemoveIcons(IEnumerable icons) + { + foreach (var icon in icons) + { + icon.Dispose(); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == IconProperty) + { + _impl?.SetIcon(Icon.PlatformImpl); + } + else if (change.Property == IsVisibleProperty) + { + _impl?.SetIsVisible(change.NewValue.GetValueOrDefault()); + } + else if (change.Property == ToolTipTextProperty) + { + _impl?.SetToolTipText(change.NewValue.GetValueOrDefault()); + } + else if (change.Property == MenuProperty) + { + _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault()); + } + } + + /// + /// Disposes the tray icon (removing it from the tray area). + /// + public void Dispose() => _impl?.Dispose(); + } +} diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 9614d079d9..fa5a09e245 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils return items.IndexOf(item) != -1; } - public static int Count(this IEnumerable items) + public static bool TryGetCountFast(this IEnumerable items, out int count) { if (items != null) { if (items is ICollection collection) { - return collection.Count; + count = collection.Count; + return true; } else if (items is IReadOnlyCollection readOnly) { - return readOnly.Count; - } - else - { - return Enumerable.Count(items.Cast()); + count = readOnly.Count; + return true; } } + + count = 0; + return false; + } + + public static int Count(this IEnumerable items) + { + if (TryGetCountFast(items, out var count)) + { + return count; + } + else if (items != null) + { + return Enumerable.Count(items.Cast()); + } else { return 0; diff --git a/src/Avalonia.Controls/WindowTransparencyLevel.cs b/src/Avalonia.Controls/WindowTransparencyLevel.cs index ce7c03efbb..f416b5de91 100644 --- a/src/Avalonia.Controls/WindowTransparencyLevel.cs +++ b/src/Avalonia.Controls/WindowTransparencyLevel.cs @@ -20,6 +20,11 @@ /// /// The window background is a blur-behind with a high blur radius. This level may fallback to Blur. /// - AcrylicBlur + AcrylicBlur, + + /// + /// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11 + /// + Mica } } diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 67b832318a..ada63f5326 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote private static DetachableTransportConnection s_lastWindowTransport; private static PreviewerWindowImpl s_lastWindow; public static List PreFlightMessages = new List(); - + + public ITrayIconImpl CreateTrayIcon() => null; + public IWindowImpl CreateWindow() => new WindowStub(); public IWindowImpl CreateEmbeddableWindow() diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs index 4f493bdcc2..d986a11c45 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs @@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.Models } } - public bool Handled { get; } + public bool Handled { get; set; } public RoutingStrategies Route { get; } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 65fd81cc78..a79816390d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels // FIXME: This leaks event handlers. Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true); + Event.RouteFinished.Subscribe(HandleRouteFinished); + _isRegistered = true; } } @@ -92,6 +94,30 @@ namespace Avalonia.Diagnostics.ViewModels else handler(); } + + private void HandleRouteFinished(RoutedEventArgs e) + { + if (!_isRegistered || IsEnabled == false) + return; + if (e.Source is IVisual v && BelongsToDevTool(v)) + return; + + var s = e.Source; + var handled = e.Handled; + var route = e.Route; + + void handler() + { + if (_currentEvent != null && handled) + { + var linkIndex = _currentEvent.EventChain.Count - 1; + var link = _currentEvent.EventChain[linkIndex]; + + link.Handled = true; + _currentEvent.HandledBy = link; + } + } + } private static bool BelongsToDevTool(IVisual v) { diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs index 32df2f8745..8069300922 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs @@ -63,8 +63,8 @@ namespace Avalonia.Diagnostics.ViewModels { if (EventChain.Count > 0) { - var prevLink = EventChain[EventChain.Count-1]; - + var prevLink = EventChain[EventChain.Count - 1]; + if (prevLink.Route != link.Route) { link.BeginsNewRoute = true; @@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels } EventChain.Add(link); + if (HandledBy == null && link.Handled) HandledBy = link; } diff --git a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs index 5d7619d184..2dc377a242 100644 --- a/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs +++ b/src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Avalonia.Controls; using Avalonia.Markup.Xaml; @@ -42,7 +43,8 @@ namespace Avalonia.Dialogs private static void ShellExec(string cmd, bool waitForExit = true) { - var escapedArgs = cmd.Replace("\"", "\\\""); + var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\") + .Replace("\"", "\\\\\\\""); using (var process = Process.Start( new ProcessStartInfo diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 7996a94dd0..c14539d7bf 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop /// This class uses synchronous execution at DBus connection establishment stage /// then switches to using AvaloniaSynchronizationContext /// - class DBusSyncContext : SynchronizationContext + private class DBusSyncContext : SynchronizationContext { private SynchronizationContext _ctx; private object _lock = new object(); @@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop public static Connection TryInitialize(string dbusAddress = null) { - if (Connection != null) - return Connection; + return Connection ?? TryCreateNewConnection(dbusAddress); + } + + public static Connection TryCreateNewConnection(string dbusAddress = null) + { var oldContext = SynchronizationContext.Current; try { diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index b5e35db969..9e426688d8 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -8,6 +8,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using Tmds.DBus; #pragma warning disable 1998 @@ -16,51 +17,78 @@ namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) + public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) { if (DBusHelper.Connection == null) return null; return new DBusMenuExporterImpl(DBusHelper.Connection, xid); } + + public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) + { + return new DBusMenuExporterImpl(currentConection, path); + } + + public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" + + Guid.NewGuid().ToString("N"); - class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { private readonly Connection _dbus; private readonly uint _xid; - private IRegistrar _registar; + private IRegistrar _registrar; private bool _disposed; private uint _revision = 1; private NativeMenu _menu; - private Dictionary _idsToItems = new Dictionary(); - private Dictionary _itemsToIds = new Dictionary(); + private readonly Dictionary _idsToItems = new Dictionary(); + private readonly Dictionary _itemsToIds = new Dictionary(); private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; + private bool _appMenu = true; + public DBusMenuExporterImpl(Connection dbus, IntPtr xid) { _dbus = dbus; _xid = (uint)xid.ToInt32(); - ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString().Replace("-", "")); + ObjectPath = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); Init(); } + public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + { + _dbus = dbus; + _appMenu = false; + ObjectPath = path; + SetNativeMenu(new NativeMenu()); + Init(); + } + async void Init() { try { - await _dbus.RegisterObjectAsync(this); - _registar = DBusHelper.Connection.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed) - await _registar.RegisterWindowAsync(_xid, ObjectPath); + if (_appMenu) + { + await _dbus.RegisterObjectAsync(this); + _registrar = DBusHelper.Connection.CreateProxy( + "com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registrar.RegisterWindowAsync(_xid, ObjectPath); + } + else + { + await _dbus.RegisterObjectAsync(this); + } } catch (Exception e) { - Console.Error.WriteLine(e); + Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) + ?.Log(this, e.Message); + // It's not really important if this code succeeds, // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that @@ -75,7 +103,7 @@ namespace Avalonia.FreeDesktop _disposed = true; _dbus.UnregisterObject(this); // Fire and forget - _registar?.UnregisterWindowAsync(_xid); + _registrar?.UnregisterWindowAsync(_xid); } @@ -248,17 +276,24 @@ namespace Avalonia.FreeDesktop if (item.ToggleType != NativeMenuItemToggleType.None) return item.IsChecked ? 1 : 0; } - + if (name == "icon-data") { if (item.Icon != null) { - var ms = new MemoryStream(); - item.Icon.Save(ms); - return ms.ToArray(); + var loader = AvaloniaLocator.Current.GetService(); + + if (loader != null) + { + var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); + + using var ms = new MemoryStream(); + icon.Save(ms); + return ms.ToArray(); + } } } - + if (name == "children-display") return menu != null ? "submenu" : null; } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs new file mode 100644 index 0000000000..a7cc4f4cc2 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -0,0 +1,457 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Avalonia.Controls.Platform; +using Avalonia.Logging; +using Avalonia.Platform; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] + +[assembly: + InternalsVisibleTo( + "Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] + +namespace Avalonia.FreeDesktop +{ + internal class DBusTrayIconImpl : ITrayIconImpl + { + private static int s_trayIconInstanceId; + + private readonly ObjectPath _dbusMenuPath; + private readonly Connection? _connection; + private IDisposable? _serviceWatchDisposable; + + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; + private IStatusNotifierWatcher? _statusNotifierWatcher; + private DbusPixmap _icon; + + private string? _sysTrayServiceName; + private string? _tooltipText; + private bool _isDisposed; + private bool _serviceConnected; + private bool _isVisible = true; + + public bool IsActive { get; private set; } + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + public Func? IconConverterDelegate { get; set; } + + public DBusTrayIconImpl() + { + _connection = DBusHelper.TryCreateNewConnection(); + + if (_connection is null) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + + return; + } + + IsActive = true; + + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + + WatchAsync(); + } + + private void InitializeSNWService() + { + if (_connection is null || _isDisposed) return; + + try + { + _statusNotifierWatcher = _connection.CreateProxy( + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + } + catch + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, + "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it."); + + return; + } + + _serviceConnected = true; + } + + private async void WatchAsync() + { + try + { + _serviceWatchDisposable = + await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, + $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}"); + } + } + + private void OnNameChange(ServiceOwnerChangedEventArgs obj) + { + if (_isDisposed) + return; + + if (!_serviceConnected & obj.NewOwner != null) + { + _serviceConnected = true; + InitializeSNWService(); + + DestroyTrayIcon(); + + if (_isVisible) + { + CreateTrayIcon(); + } + } + else if (_serviceConnected & obj.NewOwner is null) + { + DestroyTrayIcon(); + _serviceConnected = false; + } + } + + private void CreateTrayIcon() + { + if (_connection is null || !_serviceConnected || _isDisposed) + return; + + var pid = Process.GetCurrentProcess().Id; + var tid = s_trayIconInstanceId++; + + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); + + try + { + _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + _connection.RegisterServiceAsync(_sysTrayServiceName); + _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, $"Error creating a DBus tray icon: {e}."); + + _serviceConnected = false; + } + + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + + _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; + } + + private void DestroyTrayIcon() + { + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) + return; + + _connection.UnregisterObject(_statusNotifierItemDbusObj); + _connection.UnregisterServiceAsync(_sysTrayServiceName); + } + + public void Dispose() + { + IsActive = false; + _isDisposed = true; + DestroyTrayIcon(); + _connection?.Dispose(); + _serviceWatchDisposable?.Dispose(); + } + + public void SetIcon(IWindowIconImpl? icon) + { + if (_isDisposed || IconConverterDelegate is null) + return; + + if (icon is null) + { + _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); + return; + } + + var x11iconData = IconConverterDelegate(icon); + + if (x11iconData.Length == 0) return; + + var w = (int)x11iconData[0]; + var h = (int)x11iconData[1]; + + var pixLength = w * h; + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; + + for (var i = 0; i < pixLength; i++) + { + var rawPixel = x11iconData[i + 2]; + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); + } + + _icon = new DbusPixmap(w, h, pixByteArray); + _statusNotifierItemDbusObj?.SetIcon(_icon); + } + + public void SetIsVisible(bool visible) + { + if (_isDisposed) + return; + + switch (visible) + { + case true when !_isVisible: + DestroyTrayIcon(); + CreateTrayIcon(); + break; + case false when _isVisible: + DestroyTrayIcon(); + break; + } + + _isVisible = visible; + } + + public void SetToolTipText(string? text) + { + if (_isDisposed || text is null) + return; + _tooltipText = text; + _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); + } + } + + /// + /// DBus Object used for setting system tray icons. + /// + /// + /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html + /// + internal class StatusNotifierItemDbusObj : IStatusNotifierItem + { + private readonly StatusNotifierItemProperties _backingProperties; + public event Action? OnTitleChanged; + public event Action? OnIconChanged; + public event Action? OnAttentionIconChanged; + public event Action? OnOverlayIconChanged; + public event Action? OnTooltipChanged; + public Action? NewStatusAsync { get; set; } + public Action? ActivationDelegate { get; set; } + public ObjectPath ObjectPath { get; } + + public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + { + ObjectPath = new ObjectPath($"/StatusNotifierItem"); + + _backingProperties = new StatusNotifierItemProperties + { + Menu = dbusmenuPath, // Needs a dbus menu somehow + ToolTip = new ToolTip("") + }; + + InvalidateAll(); + } + + public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; + + public Task ActivateAsync(int x, int y) + { + ActivationDelegate?.Invoke(); + return Task.CompletedTask; + } + + public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; + + public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + + public void InvalidateAll() + { + OnTitleChanged?.Invoke(); + OnIconChanged?.Invoke(); + OnOverlayIconChanged?.Invoke(); + OnAttentionIconChanged?.Invoke(); + OnTooltipChanged?.Invoke(); + } + + public Task WatchNewTitleAsync(Action handler, Action onError) + { + OnTitleChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); + } + + public Task WatchNewIconAsync(Action handler, Action onError) + { + OnIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); + } + + public Task WatchNewAttentionIconAsync(Action handler, Action onError) + { + OnAttentionIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); + } + + public Task WatchNewOverlayIconAsync(Action handler, Action onError) + { + OnOverlayIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); + } + + public Task WatchNewToolTipAsync(Action handler, Action onError) + { + OnTooltipChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); + } + + public Task WatchNewStatusAsync(Action handler, Action onError) + { + NewStatusAsync += handler; + return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); + } + + public Task GetAsync(string prop) + { + return Task.FromResult(prop switch + { + nameof(_backingProperties.Category) => _backingProperties.Category, + nameof(_backingProperties.Id) => _backingProperties.Id, + nameof(_backingProperties.Menu) => _backingProperties.Menu, + nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, + nameof(_backingProperties.Status) => _backingProperties.Status, + nameof(_backingProperties.Title) => _backingProperties.Title, + nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, + _ => null + }); + } + + public Task GetAllAsync() => Task.FromResult(_backingProperties); + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task WatchPropertiesAsync(Action handler) => + Task.FromResult(Disposable.Empty); + + public void SetIcon(DbusPixmap dbusPixmap) + { + _backingProperties.IconPixmap = new[] { dbusPixmap }; + InvalidateAll(); + } + + public void SetTitleAndTooltip(string? text) + { + if (text is null) + return; + + _backingProperties.Id = text; + _backingProperties.Category = "ApplicationStatus"; + _backingProperties.Status = text; + _backingProperties.Title = text; + _backingProperties.ToolTip = new ToolTip(text); + + InvalidateAll(); + } + } + + [DBusInterface("org.kde.StatusNotifierWatcher")] + internal interface IStatusNotifierWatcher : IDBusObject + { + Task RegisterStatusNotifierItemAsync(string Service); + Task RegisterStatusNotifierHostAsync(string Service); + } + + [DBusInterface("org.kde.StatusNotifierItem")] + internal interface IStatusNotifierItem : IDBusObject + { + Task ContextMenuAsync(int x, int y); + Task ActivateAsync(int x, int y); + Task SecondaryActivateAsync(int x, int y); + Task ScrollAsync(int delta, string orientation); + Task WatchNewTitleAsync(Action handler, Action onError); + Task WatchNewIconAsync(Action handler, Action onError); + Task WatchNewAttentionIconAsync(Action handler, Action onError); + Task WatchNewOverlayIconAsync(Action handler, Action onError); + Task WatchNewToolTipAsync(Action handler, Action onError); + Task WatchNewStatusAsync(Action handler, Action onError); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + // This class is used by Tmds.Dbus to ferry properties + // from the SNI spec. + // Don't change this to actual C# properties since + // Tmds.Dbus will get confused. + [Dictionary] + internal class StatusNotifierItemProperties + { + public string? Category; + + public string? Id; + + public string? Title; + + public string? Status; + + public ObjectPath Menu; + + public DbusPixmap[]? IconPixmap; + + public ToolTip ToolTip; + } + + internal struct ToolTip + { + public readonly string First; + public readonly DbusPixmap[] Second; + public readonly string Third; + public readonly string Fourth; + + private static readonly DbusPixmap[] s_blank = + { + new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) + }; + + public ToolTip(string message) : this("", s_blank, message, "") + { + } + + public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + } + + internal readonly struct DbusPixmap + { + public readonly int Width; + public readonly int Height; + public readonly byte[] Data; + + public DbusPixmap(int width, int height, byte[] data) + { + Width = width; + Height = height; + Data = data; + } + + public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 }); + } +} diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index fca2a1336f..0ca2733cde 100644 --- a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -51,6 +51,8 @@ namespace Avalonia.Headless public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); + + public ITrayIconImpl CreateTrayIcon() => null; } internal static void Initialize() diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 89efa6af0c..4431e108ed 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -9,14 +9,15 @@ using Avalonia.Threading; namespace Avalonia.Native { - class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter + internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { - private IAvaloniaNativeFactory _factory; + private readonly IAvaloniaNativeFactory _factory; private bool _resetQueued = true; - private bool _exported = false; - private IAvnWindow _nativeWindow; + private bool _exported; + private readonly IAvnWindow _nativeWindow; private NativeMenu _menu; private __MicroComIAvnMenuProxy _nativeMenu; + private readonly IAvnTrayIcon _trayIcon; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -33,13 +34,21 @@ namespace Avalonia.Native DoLayoutReset(); } + public AvaloniaNativeMenuExporter(IAvnTrayIcon trayIcon, IAvaloniaNativeFactory factory) + { + _factory = factory; + _trayIcon = trayIcon; + + DoLayoutReset(); + } + public bool IsNativeMenuExported => _exported; public event EventHandler OnIsNativeMenuExportedChanged; public void SetNativeMenu(NativeMenu menu) { - _menu = menu == null ? new NativeMenu() : menu; + _menu = menu ?? new NativeMenu(); DoLayoutReset(true); } @@ -82,15 +91,22 @@ namespace Avalonia.Native if (_nativeWindow is null) { - var appMenu = NativeMenu.GetMenu(Application.Current); + if (_trayIcon is null) + { + var appMenu = NativeMenu.GetMenu(Application.Current); - if (appMenu == null) + if (appMenu == null) + { + appMenu = CreateDefaultAppMenu(); + NativeMenu.SetMenu(Application.Current, appMenu); + } + + SetMenu(appMenu); + } + else if (_menu != null) { - appMenu = CreateDefaultAppMenu(); - NativeMenu.SetMenu(Application.Current, appMenu); + SetMenu(_trayIcon, _menu); } - - SetMenu(appMenu); } else { @@ -118,7 +134,7 @@ namespace Avalonia.Native var appMenuHolder = menuItem?.Parent; - if (menu.Parent is null) + if (menuItem is null) { menuItem = new NativeMenuItem(); } @@ -136,7 +152,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory); + _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); _nativeMenu.Initialize(this, appMenuHolder, ""); @@ -171,5 +187,26 @@ namespace Avalonia.Native avnWindow.SetMainMenu(_nativeMenu); } } + + private void SetMenu(IAvnTrayIcon trayIcon, NativeMenu menu) + { + var setMenu = false; + + if (_nativeMenu is null) + { + _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory); + + _nativeMenu.Initialize(this, menu, ""); + + setMenu = true; + } + + _nativeMenu.Update(_factory, menu); + + if(setMenu) + { + trayIcon.SetMenu(_nativeMenu); + } + } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index a7d05e416f..eaf4d0e2e4 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -134,6 +134,11 @@ namespace Avalonia.Native } } + public ITrayIconImpl CreateTrayIcon () + { + return new TrayIconImpl(_factory); + } + public IWindowImpl CreateWindow() { return new WindowImpl(_factory, _options, _platformGl); diff --git a/src/Avalonia.Native/TrayIconImpl.cs b/src/Avalonia.Native/TrayIconImpl.cs new file mode 100644 index 0000000000..abcc61d950 --- /dev/null +++ b/src/Avalonia.Native/TrayIconImpl.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +using Avalonia.Controls.Platform; +using Avalonia.Native.Interop; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Native +{ + internal class TrayIconImpl : ITrayIconImpl + { + private readonly IAvnTrayIcon _native; + + public TrayIconImpl(IAvaloniaNativeFactory factory) + { + _native = factory.CreateTrayIcon(); + + MenuExporter = new AvaloniaNativeMenuExporter(_native, factory); + } + + public Action? OnClicked { get; set; } + + public void Dispose() + { + _native.Dispose(); + } + + public unsafe void SetIcon(IWindowIconImpl? icon) + { + if (icon is null) + { + _native.SetIcon(null, IntPtr.Zero); + } + else + { + using (var ms = new MemoryStream()) + { + icon.Save(ms); + + var imageData = ms.ToArray(); + + fixed (void* ptr = imageData) + { + _native.SetIcon(ptr, new IntPtr(imageData.Length)); + } + } + } + } + + public void SetToolTipText(string? text) + { + // NOP + } + + public void SetIsVisible(bool visible) + { + _native.SetIsVisible(visible.AsComBool()); + } + + public INativeMenuExporter? MenuExporter { get; } + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 70d85dacdd..00c54750a4 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -427,6 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); + HRESULT CreateTrayIcon(IAvnTrayIcon** ppv); } [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] @@ -665,6 +666,14 @@ interface IAvnGlSurfaceRenderingSession : IUnknown HRESULT GetScaling(double* ret); } +[uuid(60992d19-38f0-4141-a0a9-76ac303801f3)] +interface IAvnTrayIcon : IUnknown +{ + HRESULT SetIcon(void* data, size_t length); + HRESULT SetMenu(IAvnMenu* menu); + HRESULT SetIsVisible(bool isVisible); +} + [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)] interface IAvnMenu : IUnknown { diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index fe4cd48e72..fac8ca51f8 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -21,7 +21,8 @@ MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" IsLightDismissEnabled="True"> - diff --git a/src/Avalonia.Themes.Default/CheckBox.xaml b/src/Avalonia.Themes.Default/CheckBox.xaml index 5e10b319a7..75d6f853be 100644 --- a/src/Avalonia.Themes.Default/CheckBox.xaml +++ b/src/Avalonia.Themes.Default/CheckBox.xaml @@ -41,6 +41,7 @@ ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" + RecognizesAccessKey="True" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}" diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 754aadac83..262f63e2d2 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -69,7 +69,8 @@ MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" IsLightDismissEnabled="True"> - diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 0df4866184..987b72aaa2 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -1,4 +1,5 @@ diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index 9af4f5a910..9468cc5535 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -1,5 +1,13 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml index 25c71e3493..1e573913b9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml @@ -1,8 +1,13 @@ + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + + parent.Children.Remove(b1); + + Assert.Null(b1.Background); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + } + } + + [Fact] + public void Style_Can_Use_NthLastChild_Selector_After_Reoder() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + + var parent = window.FindControl("parent"); + var b1 = window.FindControl("b1"); + var b2 = window.FindControl("b2"); + + Assert.Equal(Brushes.Red, b1.Background); + Assert.Null(b2.Background); + + parent.Children.Remove(b1); + + Assert.Null(b1.Background); + Assert.Null(b2.Background); + + parent.Children.Add(b1); + + Assert.Null(b1.Background); + Assert.Equal(Brushes.Red, b2.Background); + } + } + + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ListBox() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.VirtualizationMode = ItemVirtualizationMode.Simple; + list.Items = collection; + + window.Show(); + + IEnumerable GetColors() => list.Presenter.Panel.Children.Cast().Select(t => t.Background); + + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); + + collection.Remove(Brushes.Green); + + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); + } + } + + [Fact] + public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var collection = new ObservableCollection() + { + Brushes.Red, Brushes.Green, Brushes.Blue + }; + + var list = window.FindControl("list"); + list.Items = collection; + + window.Show(); + + IEnumerable GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count) + .Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground); + + Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors()); + + collection.Remove(Brushes.Green); + + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors()); + + collection.Add(Brushes.Violet); + collection.Add(Brushes.Black); + + Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors()); + } + } + [Fact] public void Style_Can_Use_Or_Selector_1() { diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs index 8002b572bd..14f5b7c6c7 100644 --- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs @@ -78,6 +78,54 @@ namespace Avalonia.Direct2D1.RenderTests.Controls CompareImages(); } + [Fact] + public async Task GeometryClip_With_Transform() + { + var target = new Border + { + Background = Brushes.White, + Width = 200, + Height = 200, + Child = new CustomRenderer((control, context) => + { + using (var transform = context.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (var clip = context.PushClip(new Rect(0, 0, 100, 100))) + { + context.FillRectangle(Brushes.Blue, new Rect(0, 0, 200, 200)); + } + + context.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100)); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task Clip_With_Transform() + { + var target = new Border + { + Background = Brushes.White, + Width = 200, + Height = 200, + Child = new CustomRenderer((control, context) => + { + using (var transform = context.PushPreTransform(Matrix.CreateTranslation(100, 100))) + using (var clip = context.PushClip(new Rect(0, 0, 100, 100))) + { + context.FillRectangle(Brushes.Blue, new Rect(0, 0, 200, 200)); + } + + context.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100)); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + [Fact] public async Task Opacity() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs index 68813f28ab..ddf4a36dcd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs @@ -6,18 +6,24 @@ namespace Avalonia.Skia.UnitTests.Media { public class SKTypefaceCollectionCacheTests { - [Fact] - public void Should_Get_Near_Matching_Typeface() + private const string s_notoMono = + "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"; + + [InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)] + [InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)] + [InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)] + [Theory] + public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle) { using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - var notoMono = - new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - - var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); + var fontFamily = new FontFamily(familyName); + + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily); - Assert.Equal("Noto Mono", - notoMonoCollection.Get(new Typeface(notoMono, weight: FontWeight.Bold)).FamilyName); + var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName; + + Assert.Equal("Noto Mono", actual); } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index bc003537f4..eb18030ca8 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -126,6 +126,11 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } + public ITrayIconImpl CreateTrayIcon() + { + return null; + } + private static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl { mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); diff --git a/tests/TestFiles/Direct2D1/Controls/CustomRender/Clip_With_Transform.expected.png b/tests/TestFiles/Direct2D1/Controls/CustomRender/Clip_With_Transform.expected.png new file mode 100644 index 0000000000..ce74202234 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/CustomRender/Clip_With_Transform.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Controls/CustomRender/GeometryClip_With_Transform.expected.png b/tests/TestFiles/Direct2D1/Controls/CustomRender/GeometryClip_With_Transform.expected.png new file mode 100644 index 0000000000..ce74202234 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Controls/CustomRender/GeometryClip_With_Transform.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/CustomRender/Clip_With_Transform.expected.png b/tests/TestFiles/Skia/Controls/CustomRender/Clip_With_Transform.expected.png new file mode 100644 index 0000000000..7c0bfa6fdc Binary files /dev/null and b/tests/TestFiles/Skia/Controls/CustomRender/Clip_With_Transform.expected.png differ diff --git a/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip_With_Transform.expected.png b/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip_With_Transform.expected.png new file mode 100644 index 0000000000..7c0bfa6fdc Binary files /dev/null and b/tests/TestFiles/Skia/Controls/CustomRender/GeometryClip_With_Transform.expected.png differ