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..bd93de0e78 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -231,6 +231,8 @@ public: virtual HRESULT GetFrameSize(AvnSize* ret) override { + START_COM_CALL; + @autoreleasepool { if(ret == nullptr) @@ -1541,7 +1543,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return pt; } -- (AvnPoint)toAvnPoint:(CGPoint)p ++ (AvnPoint)toAvnPoint:(CGPoint)p { AvnPoint result; @@ -1598,7 +1600,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 +1945,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 +2378,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/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..ee42e7a54b 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}"> 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/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/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.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..85fd55800a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -201,21 +201,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 +259,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 +283,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 +300,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); } 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/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/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/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/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index a5cdeefb0e..856bcd1079 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Primitives public class Popup : Control, IVisualTreeHost, IPopupHostProvider { public static readonly StyledProperty WindowManagerAddShadowHintProperty = - AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), true); + AvaloniaProperty.Register(nameof(WindowManagerAddShadowHint), false); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 01200e87e3..0ff8fcbd28 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -588,14 +588,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/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs new file mode 100644 index 0000000000..6bfddfa877 --- /dev/null +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Collections; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Platform; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Controls +{ + public sealed class TrayIcons : AvaloniaList + { + } + + public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable + { + private readonly ITrayIconImpl? _impl; + + private TrayIcon(ITrayIconImpl? impl) + { + if (impl != null) + { + _impl = impl; + + _impl.SetIsVisible(IsVisible); + + _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + } + } + + 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 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 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/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.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..4e23711ed4 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop public static Connection TryInitialize(string dbusAddress = null) { - if (Connection != null) - return Connection; + return Connection ?? TryGetConnection(dbusAddress); + } + + public static Connection TryGetConnection(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.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.Visuals/Media/DrawingImage.cs b/src/Avalonia.Visuals/Media/DrawingImage.cs index 56c883014a..6fa8d397a5 100644 --- a/src/Avalonia.Visuals/Media/DrawingImage.cs +++ b/src/Avalonia.Visuals/Media/DrawingImage.cs @@ -11,6 +11,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.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 6c84cfd55c..fe63fdec46 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/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.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 54a9ff733d..3dc6d5f50e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/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.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index f95d4320fe..9c37de86bf 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -23,9 +23,9 @@ namespace Avalonia.X11 private static readonly Dictionary s_mapping = new Dictionary { - {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow}, + {StandardCursorType.Arrow, CursorFontShape.XC_left_ptr}, {StandardCursorType.Cross, CursorFontShape.XC_cross}, - {StandardCursorType.Hand, CursorFontShape.XC_hand1}, + {StandardCursorType.Hand, CursorFontShape.XC_hand2}, {StandardCursorType.Help, CursorFontShape.XC_question_arrow}, {StandardCursorType.Ibeam, CursorFontShape.XC_xterm}, {StandardCursorType.No, CursorFontShape.XC_X_cursor}, @@ -67,7 +67,7 @@ namespace Avalonia.X11 { handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] - : _cursors[CursorFontShape.XC_top_left_arrow]; + : _cursors[CursorFontShape.XC_left_ptr]; } return new CursorImpl(handle); } diff --git a/src/Avalonia.X11/X11Info.cs b/src/Avalonia.X11/X11Info.cs index 3bded4cce1..9920907601 100644 --- a/src/Avalonia.X11/X11Info.cs +++ b/src/Avalonia.X11/X11Info.cs @@ -42,7 +42,7 @@ namespace Avalonia.X11 DefaultScreen = XDefaultScreen(display); BlackPixel = XBlackPixel(display, DefaultScreen); RootWindow = XRootWindow(display, DefaultScreen); - DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow); + DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_left_ptr); DefaultRootWindow = XDefaultRootWindow(display); Atoms = new X11Atoms(display); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 3a919c8814..d3aeefd088 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -52,11 +52,14 @@ namespace Avalonia.X11 XInitThreads(); Display = XOpenDisplay(IntPtr.Zero); + if (Display == IntPtr.Zero) + throw new Exception("XOpenDisplay failed"); DeferredDisplay = XOpenDisplay(IntPtr.Zero); + if (DeferredDisplay == IntPtr.Zero) + throw new Exception("XOpenDisplay failed"); + OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero); - if (Display == IntPtr.Zero) - throw new Exception("XOpenDisplay failed"); XError.Init(); Info = new X11Info(Display, DeferredDisplay, useXim); @@ -100,6 +103,12 @@ namespace Avalonia.X11 public IntPtr DeferredDisplay { get; set; } public IntPtr Display { get; set; } + + public ITrayIconImpl CreateTrayIcon () + { + return new X11TrayIconImpl(); + } + public IWindowImpl CreateWindow() { return new X11Window(this, null); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs new file mode 100644 index 0000000000..371ff75408 --- /dev/null +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -0,0 +1,367 @@ +#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.FreeDesktop; +using Avalonia.Logging; +using Avalonia.Platform; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] + +namespace Avalonia.X11 +{ + internal class X11TrayIconImpl : ITrayIconImpl + { + private static int s_trayIconInstanceId; + private readonly ObjectPath _dbusMenuPath; + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; + private readonly Connection? _connection; + private DbusPixmap _icon; + + private IStatusNotifierWatcher? _statusNotifierWatcher; + + private string? _sysTrayServiceName; + private string? _tooltipText; + private bool _isActive; + private bool _isDisposed; + private readonly bool _ctorFinished; + + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + + public X11TrayIconImpl() + { + _connection = DBusHelper.TryGetConnection(); + + if (_connection is null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + return; + } + + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + CreateTrayIcon(); + _ctorFinished = true; + } + + public async void CreateTrayIcon() + { + if (_connection is null) + return; + + try + { + _statusNotifierWatcher = _connection.CreateProxy( + "org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + } + catch + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + } + + if (_statusNotifierWatcher is null) + return; + + var pid = Process.GetCurrentProcess().Id; + var tid = s_trayIconInstanceId++; + + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); + + await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + + await _connection.RegisterServiceAsync(_sysTrayServiceName); + + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + + _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; + + _isActive = true; + } + + public async void DestroyTrayIcon() + { + if (_connection is null) + return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); + await _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isActive = false; + } + + public void Dispose() + { + _isDisposed = true; + DestroyTrayIcon(); + _connection?.Dispose(); + } + + public void SetIcon(IWindowIconImpl? icon) + { + if (_isDisposed) + return; + if (!(icon is X11IconData x11icon)) + return; + + var w = (int)x11icon.Data[0]; + var h = (int)x11icon.Data[1]; + + var pixLength = w * h; + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; + + for (var i = 0; i < pixLength; i++) + { + var rawPixel = x11icon.Data[i + 2].ToUInt32(); + 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 || !_ctorFinished) + return; + + if (visible & !_isActive) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else if (!visible & _isActive) + { + DestroyTrayIcon(); + } + } + + 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) => Task.FromResult(new object()); + + 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); + } + + [Dictionary] + // 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. + 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; + } + } +} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 14645bc415..7bc8872fa7 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -190,7 +190,7 @@ namespace Avalonia.X11 if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) - NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle); NativeControlHost = new X11NativeControlHost(_platform, this); InitializeIme(); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a82f5b9e60..1db0208310 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -49,8 +49,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XmlNamespaceInfoProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"), DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")}, + DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"), + DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List + { + "TemplateResultType" + }, DeferredContentExecutorCustomization = - runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"), + runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"), UsableDuringInitializationAttributes = { typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index f4ac681b91..8e20d65eb5 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72 +Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3 diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 483a1a5d06..07c79d7077 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates public static class TemplateContent { public static ControlTemplateResult Load(object templateContent) + { if (templateContent is Func direct) { @@ -20,5 +21,16 @@ namespace Avalonia.Markup.Xaml.Templates throw new ArgumentException(nameof(templateContent)); } + + public static TemplateResult Load(object templateContent) + { + if (templateContent is Func direct) + return (TemplateResult)direct(null); + + if (templateContent is null) + return null; + + throw new ArgumentException(nameof(templateContent)); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 83d70122b3..c48f386ffd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -15,6 +15,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) + { + return DeferredTransformationFactoryV2(builder, provider); + } + + public static Func DeferredTransformationFactoryV2(Func builder, + IServiceProvider provider) { var resourceNodes = provider.GetService().Parents .OfType().ToList(); @@ -25,7 +31,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); scope.Complete(); - return new ControlTemplateResult((IControl)obj, scope); + + if(typeof(T) == typeof(IControl)) + return new ControlTemplateResult((IControl)obj, scope); + + return new TemplateResult((T)obj, scope); }; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d812f6a059..7026e6d9ce 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -591,7 +591,7 @@ namespace Avalonia.Skia /// Configure paint wrapper for using gradient brush. /// /// Paint wrapper. - /// Target bound rect. + /// Target size. /// Gradient brush. private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush) { diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 178ee8c544..6b560ac739 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -140,7 +140,11 @@ namespace Avalonia.Skia $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); } - return new GlyphTypefaceImpl(skTypeface); + var isFakeBold = (int)typeface.Weight >= 600 && !skTypeface.IsBold; + + var isFakeItalic = typeface.Style == FontStyle.Italic && !skTypeface.IsItalic; + + return new GlyphTypefaceImpl(skTypeface, isFakeBold, isFakeItalic); } } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index ceccc481f9..9601fece25 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia { private bool _isDisposed; - public GlyphTypefaceImpl(SKTypeface typeface) + public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFakeItalic = false) { Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); @@ -52,6 +52,10 @@ namespace Avalonia.Skia 0; IsFixedPitch = Typeface.IsFixedPitch; + + IsFakeBold = isFakeBold; + + IsFakeItalic = isFakeItalic; } public Face Face { get; } @@ -86,6 +90,10 @@ namespace Avalonia.Skia /// public bool IsFixedPitch { get; } + + public bool IsFakeBold { get; } + + public bool IsFakeItalic { get; } /// public ushort GetGlyph(uint codepoint) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index e2175f1145..4130d38f17 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -217,6 +217,8 @@ namespace Avalonia.Skia s_font.Size = (float)glyphRun.FontRenderingEmSize; s_font.Typeface = typeface; + s_font.Embolden = glyphTypeface.IsFakeBold; + s_font.SkewX = glyphTypeface.IsFakeItalic ? -0.2f : 0; SKTextBlob textBlob; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 6a6bcc5715..938f4222e0 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1110,6 +1110,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetActiveWindow(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] public static extern IntPtr SetCapture(IntPtr hWnd); @@ -1197,6 +1200,9 @@ namespace Avalonia.Win32.Interop GCW_ATOM = -32 } + [DllImport("shell32", CharSet = CharSet.Auto)] + public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData); + [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")] private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); @@ -2296,4 +2302,61 @@ namespace Avalonia.Win32.Interop public uint VisibleMask; public uint DamageMask; } + + internal enum NIM : uint + { + ADD = 0x00000000, + MODIFY = 0x00000001, + DELETE = 0x00000002, + SETFOCUS = 0x00000003, + SETVERSION = 0x00000004 + } + + [Flags] + internal enum NIF : uint + { + MESSAGE = 0x00000001, + ICON = 0x00000002, + TIP = 0x00000004, + STATE = 0x00000008, + INFO = 0x00000010, + GUID = 0x00000020, + REALTIME = 0x00000040, + SHOWTIP = 0x00000080 + } + + [Flags] + internal enum NIIF : uint + { + NONE = 0x00000000, + INFO = 0x00000001, + WARNING = 0x00000002, + ERROR = 0x00000003, + USER = 0x00000004, + ICON_MASK = 0x0000000F, + NOSOUND = 0x00000010, + LARGE_ICON = 0x00000020, + RESPECT_QUIET_TIME = 0x00000080 + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class NOTIFYICONDATA + { + public int cbSize = Marshal.SizeOf(); + public IntPtr hWnd; + public int uID; + public NIF uFlags; + public int uCallbackMessage; + public IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public int dwState = 0; + public int dwStateMask = 0; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public NIIF dwInfoFlags; + } } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs new file mode 100644 index 0000000000..23395dd9b5 --- /dev/null +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.LogicalTree; +using Avalonia.Platform; +using Avalonia.Styling; +using Avalonia.Win32.Interop; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +#nullable enable + +namespace Avalonia.Win32 +{ + public class TrayIconImpl : ITrayIconImpl + { + private readonly int _uniqueId; + private static int s_nextUniqueId; + private bool _iconAdded; + private IconImpl? _icon; + private string? _tooltipText; + private readonly Win32NativeToManagedMenuExporter _exporter; + private static readonly Dictionary s_trayIcons = new Dictionary(); + private bool _disposedValue; + + public TrayIconImpl() + { + _exporter = new Win32NativeToManagedMenuExporter(); + + _uniqueId = ++s_nextUniqueId; + + s_trayIcons.Add(_uniqueId, this); + } + + public Action? OnClicked { get; set; } + + public INativeMenuExporter MenuExporter => _exporter; + + internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32())) + { + s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam); + } + } + + public void SetIcon(IWindowIconImpl? icon) + { + _icon = icon as IconImpl; + UpdateIcon(); + } + + public void SetIsVisible(bool visible) + { + UpdateIcon(!visible); + } + + public void SetToolTipText(string? text) + { + _tooltipText = text; + UpdateIcon(!_iconAdded); + } + + private void UpdateIcon(bool remove = false) + { + var iconData = new NOTIFYICONDATA() + { + hWnd = Win32Platform.Instance.Handle, + uID = _uniqueId, + uFlags = NIF.TIP | NIF.MESSAGE, + uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE, + hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon, + szTip = _tooltipText ?? "" + }; + + if (!remove) + { + iconData.uFlags |= NIF.ICON; + + if (!_iconAdded) + { + Shell_NotifyIcon(NIM.ADD, iconData); + _iconAdded = true; + } + else + { + Shell_NotifyIcon(NIM.MODIFY, iconData); + } + } + else + { + Shell_NotifyIcon(NIM.DELETE, iconData); + _iconAdded = false; + } + } + + private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE) + { + // Determine the type of message and call the matching event handlers + switch (lParam.ToInt32()) + { + case (int)WindowsMessage.WM_LBUTTONUP: + OnClicked?.Invoke(); + break; + + case (int)WindowsMessage.WM_RBUTTONUP: + OnRightClicked(); + break; + } + + return IntPtr.Zero; + } + + return DefWindowProc(hWnd, msg, wParam, lParam); + } + + private void OnRightClicked() + { + var _trayMenu = new TrayPopupRoot() + { + SystemDecorations = SystemDecorations.None, + SizeToContent = SizeToContent.WidthAndHeight, + Background = null, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + Content = new TrayIconMenuFlyoutPresenter() + { + Items = _exporter.GetMenu() + } + }; + + GetCursorPos(out POINT pt); + + _trayMenu.Position = new PixelPoint(pt.X, pt.Y); + + _trayMenu.Show(); + } + + /// + /// Custom Win32 window messages for the NotifyIcon + /// + private enum CustomWindowsMessage : uint + { + WM_TRAYICON = WindowsMessage.WM_APP + 1024, + WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024 + } + + private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable + { + Type IStyleable.StyleKey => typeof(MenuFlyoutPresenter); + + public override void Close() + { + // DefaultMenuInteractionHandler calls this + var host = this.FindLogicalAncestorOfType(); + if (host != null) + { + SelectedIndex = -1; + host.Close(); + } + } + } + + private class TrayPopupRoot : Window + { + private readonly ManagedPopupPositioner _positioner; + + public TrayPopupRoot() + { + _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize)); + Topmost = true; + + Deactivated += TrayPopupRoot_Deactivated; + + ShowInTaskbar = false; + + ShowActivated = true; + } + + private void TrayPopupRoot_Deactivated(object sender, EventArgs e) + { + Close(); + } + + private void MoveResize(PixelPoint position, Size size, double scaling) + { + PlatformImpl!.Move(position); + PlatformImpl!.Resize(size, PlatformResizeReason.Layout); + } + + protected override void ArrangeCore(Rect finalRect) + { + base.ArrangeCore(finalRect); + + _positioner.Update(new PopupPositionerParameters + { + Anchor = PopupAnchor.TopLeft, + Gravity = PopupGravity.BottomRight, + AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)), + Size = finalRect.Size, + ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY, + }); + } + + private class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup + { + private readonly Action _moveResize; + private readonly Window _hiddenWindow; + + public TrayIconManagedPopupPositionerPopupImplHelper(Action moveResize) + { + _moveResize = moveResize; + _hiddenWindow = new Window(); + } + + public IReadOnlyList Screens => + _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo( + s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList(); + + public Rect ParentClientAreaScreenGeometry + { + get + { + var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft; + var size = _hiddenWindow.Screens.Primary.Bounds.Size; + return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity); + } + } + + public void MoveAndResize(Point devicePoint, Size virtualSize) + { + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity); + } + + public double Scaling => _hiddenWindow.Screens.Primary.PixelDensity; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + UpdateIcon(true); + + _disposedValue = true; + } + } + + ~TrayIconImpl() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index e70ea52106..289c100d51 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -27,7 +27,7 @@ namespace Avalonia.Win32 if (egl != null && opts?.UseWindowsUIComposition == true) { - WinUICompositorConnection.TryCreateAndRegister(egl); + WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); } return egl; diff --git a/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs new file mode 100644 index 0000000000..fa6f9927b5 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Platform; + +#nullable enable + +namespace Avalonia.Win32 +{ + internal class Win32NativeToManagedMenuExporter : INativeMenuExporter + { + private NativeMenu? _nativeMenu; + + public void SetNativeMenu(NativeMenu? nativeMenu) + { + _nativeMenu = nativeMenu; + } + + private IEnumerable Populate(NativeMenu nativeMenu) + { + foreach (var menuItem in nativeMenu.Items) + { + if (menuItem is NativeMenuItemSeparator) + { + yield return new MenuItem { Header = "-" }; + } + else if (menuItem is NativeMenuItem item) + { + var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter }; + + if (item.Menu != null) + { + newItem.Items = Populate(item.Menu); + } + else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge) + { + newItem.Click += (_, __) => bridge.RaiseClicked(); + } + + yield return newItem; + } + } + } + + public IEnumerable? GetMenu() + { + if (_nativeMenu != null) + { + return Populate(_nativeMenu); + } + + return null; + } + } +} diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index c011a458c3..2947493c41 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -89,6 +89,13 @@ namespace Avalonia /// This is recommended if you need to use AcrylicBlur or acrylic in your applications. /// public bool UseWindowsUIComposition { get; set; } = true; + + /// + /// When enabled, create rounded corner blur brushes + /// If set to null the brushes will be created using default settings (sharp corners) + /// This can be useful when you need a rounded-corner blurred Windows 10 app, or borderless Windows 11 app + /// + public float? CompositionBackdropCornerRadius { get; set; } } } @@ -108,6 +115,10 @@ namespace Avalonia.Win32 CreateMessageWindow(); } + internal static Win32Platform Instance => s_instance; + + internal IntPtr Handle => _hwnd; + /// /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion. /// @@ -261,6 +272,8 @@ namespace Avalonia.Win32 } } } + + TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam); return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); } @@ -293,6 +306,11 @@ namespace Avalonia.Win32 } } + public ITrayIconImpl CreateTrayIcon () + { + return new TrayIconImpl(); + } + public IWindowImpl CreateWindow() { return new WindowImpl(); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs index 4ae9c08410..a09918a3a6 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs @@ -13,6 +13,8 @@ namespace Avalonia.Win32.WinRT.Composition { private EglContext _syncContext; private readonly object _pumpLock; + private readonly IVisual _micaVisual; + private readonly ICompositionRoundedRectangleGeometry _roundedRectangleGeometry; private readonly IVisual _blurVisual; private ICompositionTarget _compositionTarget; private IVisual _contentVisual; @@ -28,11 +30,14 @@ namespace Avalonia.Win32.WinRT.Composition object pumpLock, ICompositionTarget compositionTarget, ICompositionDrawingSurfaceInterop surfaceInterop, - IVisual contentVisual, IVisual blurVisual) + IVisual contentVisual, IVisual blurVisual, IVisual micaVisual, + ICompositionRoundedRectangleGeometry roundedRectangleGeometry) { _compositor = compositor.CloneReference(); _syncContext = syncContext; _pumpLock = pumpLock; + _micaVisual = micaVisual; + _roundedRectangleGeometry = roundedRectangleGeometry; _blurVisual = blurVisual.CloneReference(); _compositionTarget = compositionTarget.CloneReference(); _contentVisual = contentVisual.CloneReference(); @@ -48,6 +53,7 @@ namespace Avalonia.Win32.WinRT.Composition { _surfaceInterop.Resize(new UnmanagedMethods.POINT { X = size.Width, Y = size.Height }); _contentVisual.SetSize(new Vector2(size.Width, size.Height)); + _roundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); _size = size; } } @@ -72,10 +78,13 @@ namespace Avalonia.Win32.WinRT.Composition _surfaceInterop.EndDraw(); } - public void SetBlur(bool enable) + public void SetBlur(BlurEffect blurEffect) { using (_syncContext.EnsureLocked()) - _blurVisual.SetIsVisible(enable ? 1 : 0); + { + _blurVisual.SetIsVisible(blurEffect == BlurEffect.Acrylic ? 1 : 0); + _micaVisual?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); + } } public IDisposable BeginTransaction() diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index bc8f5a606c..57b0f71306 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; @@ -16,7 +17,9 @@ namespace Avalonia.Win32.WinRT.Composition { class WinUICompositorConnection : IRenderTimer { + private readonly float? _backdropCornerRadius; private readonly EglContext _syncContext; + private readonly ICompositionBrush _micaBrush; private ICompositor _compositor; private ICompositor2 _compositor2; private ICompositor5 _compositor5; @@ -28,10 +31,11 @@ namespace Avalonia.Win32.WinRT.Composition private ICompositionBrush _blurBrush; private object _pumpLock = new object(); - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock) + public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius) { _gl = gl; _pumpLock = pumpLock; + _backdropCornerRadius = backdropCornerRadius; _syncContext = _gl.PrimaryEglContext; _angle = (AngleWin32EglDisplay)_gl.Display; _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); @@ -42,13 +46,13 @@ namespace Avalonia.Win32.WinRT.Composition using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); _device = _compositorInterop.CreateGraphicsDevice(device); - _blurBrush = CreateBlurBrush(); - + _blurBrush = CreateAcrylicBlurBackdropBrush(); + _micaBrush = CreateMicaBackdropBrush(); } public EglPlatformOpenGlInterface Egl => _gl; - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle) + static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius) { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -63,7 +67,7 @@ namespace Avalonia.Win32.WinRT.Composition dwSize = Marshal.SizeOf(), threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT }); - connect = new WinUICompositorConnection(angle, pumpLock); + connect = new WinUICompositorConnection(angle, pumpLock, backdropCornerRadius); AvaloniaLocator.CurrentMutable.BindToSelf(connect); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); tcs.SetResult(true); @@ -130,7 +134,8 @@ namespace Avalonia.Win32.WinRT.Composition } } - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle) + public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, + float? backdropCornerRadius) { const int majorRequired = 10; const int buildRequired = 17134; @@ -143,7 +148,7 @@ namespace Avalonia.Win32.WinRT.Composition { try { - TryCreateAndRegisterCore(angle); + TryCreateAndRegisterCore(angle, backdropCornerRadius); return; } catch (Exception e) @@ -188,16 +193,37 @@ namespace Avalonia.Win32.WinRT.Composition target.SetRoot(containerVisual); - using var blur = CreateBlurVisual(); + using var blur = CreateBlurVisual(_blurBrush); + IVisual mica = null; + if (_micaBrush != null) + { + mica = CreateBlurVisual(_micaBrush); + containerChildren.InsertAtTop(mica); + } + + var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); containerChildren.InsertAtTop(blur); containerChildren.InsertAtTop(visual); - return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, blur); + return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, + blur, mica, compositionRoundedRectangleGeometry); } + private ICompositionBrush CreateMicaBackdropBrush() + { + if (Win32Platform.WindowsVersion.Build < 22000) + return null; + + using var compositorWithBlurredWallpaperBackdropBrush = + _compositor.QueryInterface(); + using var blurredWallpaperBackdropBrush = + compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); + using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface(); + return micaBackdropBrush.CloneReference(); + } - private unsafe ICompositionBrush CreateBlurBrush() + private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush() { using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( "Windows.UI.Composition.CompositionEffectSourceParameter"); @@ -207,6 +233,7 @@ namespace Avalonia.Win32.WinRT.Composition using var backDropParameterAsSource = backDropParameter.QueryInterface(); var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); + using var compositionEffectBrush = blurEffectFactory.CreateBrush(); using var backdrop = _compositor2.CreateBackdropBrush(); using var backdropBrush = backdrop.QueryInterface(); @@ -214,18 +241,39 @@ namespace Avalonia.Win32.WinRT.Composition var saturateEffect = new SaturationEffect(blurEffect); using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); using var sat = satEffectFactory.CreateBrush(); - sat.SetSourceParameter(backdropString.Handle, backdropBrush); - return sat.QueryInterface(); + compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); + return compositionEffectBrush.QueryInterface(); + } + + private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) + { + if (!_backdropCornerRadius.HasValue) + return null; + using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); + + using var compositor6 = _compositor.QueryInterface(); + using var compositionGeometry = roundedRectangleGeometry + .QueryInterface(); + + using var geometricClipWithGeometry = + compositor6.CreateGeometricClipWithGeometry(compositionGeometry); + foreach (var visual in containerVisuals) + { + visual?.SetClip(geometricClipWithGeometry.QueryInterface()); } - private unsafe IVisual CreateBlurVisual() + return roundedRectangleGeometry.CloneReference(); + } + + private unsafe IVisual CreateBlurVisual(ICompositionBrush compositionBrush) { using var spriteVisual = _compositor.CreateSpriteVisual(); using var visual = spriteVisual.QueryInterface(); using var visual2 = spriteVisual.QueryInterface(); - spriteVisual.SetBrush(_blurBrush); + spriteVisual.SetBrush(compositionBrush); visual.SetIsVisible(0); visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index f59d50860a..4ed882552b 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -15,7 +15,7 @@ namespace Avalonia.Win32.WinRT.Composition private EglPlatformOpenGlInterface _egl; private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private IRef _window; - private bool _enableBlur; + private BlurEffect _blurEffect; public WinUiCompositedWindowSurface(WinUICompositorConnection connection, IEglWindowGlPlatformSurfaceInfo info) : base() { @@ -31,7 +31,7 @@ namespace Avalonia.Win32.WinRT.Composition if (_window?.Item == null) { _window = RefCountable.Create(_connection.CreateWindow(_info.Handle)); - _window.Item.SetBlur(_enableBlur); + _window.Item.SetBlur(_blurEffect); } return new CompositionRenderTarget(_egl, _window, _info); @@ -100,10 +100,10 @@ namespace Avalonia.Win32.WinRT.Composition } } - public void SetBlur(bool enable) + public void SetBlur(BlurEffect blurEffect) { - _enableBlur = enable; - _window?.Item?.SetBlur(enable); + _blurEffect = blurEffect; + _window?.Item?.SetBlur(blurEffect); } public void Dispose() diff --git a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs index 81c0e3e185..526be6e0f0 100644 --- a/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs +++ b/src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs @@ -1,7 +1,14 @@ namespace Avalonia.Win32.WinRT { - public interface IBlurHost + public enum BlurEffect { - void SetBlur(bool enable); + None, + Acrylic, + Mica + } + + internal interface IBlurHost + { + void SetBlur(BlurEffect enable); } } diff --git a/src/Windows/Avalonia.Win32/WinRT/winrt.idl b/src/Windows/Avalonia.Win32/WinRT/winrt.idl index 929377999c..18a9a26fca 100644 --- a/src/Windows/Avalonia.Win32/WinRT/winrt.idl +++ b/src/Windows/Avalonia.Win32/WinRT/winrt.idl @@ -358,6 +358,12 @@ interface ICompositor2 : IInspectable [overload("CreateStepEasingFunction")] HRESULT CreateStepEasingFunctionWithStepCount([in] INT32 stepCount, [out] [retval] void** result); } +[uuid(0D8FB190-F122-5B8D-9FDD-543B0D8EB7F3)] +interface ICompositorWithBlurredWallpaperBackdropBrush : IInspectable +{ + HRESULT TryCreateBlurredWallpaperBackdropBrush([out] [retval] ICompositionBackdropBrush** result); +} + [uuid(08E05581-1AD1-4F97-9757-402D76E4233B)] interface ISpriteVisual : IInspectable { @@ -520,6 +526,13 @@ enum CompositionCompositeMode MinBlend, } +[contract(Windows.Foundation.UniversalApiContract, 2.0)] +[exclusiveto(Windows.UI.Composition.CompositionClip)] +[uuid(1CCD2A52-CFC7-4ACE-9983-146BB8EB6A3C)] +interface ICompositionClip : IInspectable +{ +} + [uuid(117E202D-A859-4C89-873B-C2AA566788E3)] interface IVisual : IInspectable { @@ -531,8 +544,8 @@ interface IVisual : IInspectable [propput] HRESULT BorderMode([in] CompositionBorderMode value); [propget] HRESULT CenterPoint([out] [retval] Vector3* value); [propput] HRESULT CenterPoint([in] Vector3 value); - [propget] HRESULT Clip([out] [retval]void** value); - [propput] HRESULT Clip([in] void* value); + [propget] HRESULT Clip([out] [retval] ICompositionClip** value); + [propput] HRESULT Clip([in] ICompositionClip* value); [propget] HRESULT CompositeMode([out] [retval] CompositionCompositeMode* value); [propput] HRESULT CompositeMode([in] CompositionCompositeMode value); [propget] HRESULT IsVisible([out] [retval] boolean* value); @@ -692,6 +705,99 @@ interface ICompositionScopedBatch : IInspectable [eventremove] HRESULT RemoveCompleted([in] int token); } +[contract(Windows.Foundation.UniversalApiContract, 6.0)] +[exclusiveto(Windows.UI.Composition.CompositionRoundedRectangleGeometry)] +[uuid(8770C822-1D50-4B8B-B013-7C9A0E46935F)] +interface ICompositionRoundedRectangleGeometry : IInspectable +{ + [propget] HRESULT CornerRadius([out] [retval] Vector2* value); + [propput] HRESULT CornerRadius([in] Vector2 value); + [propget] HRESULT Offset([out] [retval] Vector2* value); + [propput] HRESULT Offset([in] Vector2 value); + [propget] HRESULT Size([out] [retval] Vector2* value); + [propput] HRESULT Size([in] Vector2 value); +} + +[contract(Windows.Foundation.UniversalApiContract, 6.0)] +[exclusiveto(Windows.UI.Composition.CompositionGeometry)] +[uuid(E985217C-6A17-4207-ABD8-5FD3DD612A9D)] +interface ICompositionGeometry : IInspectable +{ + [propget] HRESULT TrimEnd([out] [retval] FLOAT* value); + [propput] HRESULT TrimEnd([in] FLOAT value); + [propget] HRESULT TrimOffset([out] [retval] FLOAT* value); + [propput] HRESULT TrimOffset([in] FLOAT value); + [propget] HRESULT TrimStart([out] [retval] FLOAT* value); + [propput] HRESULT TrimStart([in] FLOAT value); +} + +[uuid(401B61BB-0007-4363-B1F3-6BCC003FB83E)] +interface ICompositionSpriteShape : IInspectable +{ + [propget] HRESULT GetFillBrush([out] [retval] ICompositionBrush** value); + [propput] HRESULT SetFillBrush([in] ICompositionBrush* value); + [propget] HRESULT Geometry([out] [retval] ICompositionGeometry** value); + [propput] HRESULT Geometry([in] ICompositionGeometry* value); + [propget] HRESULT IsStrokeNonScaling([out] [retval] boolean* value); + [propput] HRESULT IsStrokeNonScaling([in] boolean value); + [propget] HRESULT StrokeBrush([out] [retval] ICompositionBrush** value); + [propput] HRESULT StrokeBrush([in] ICompositionBrush* value); + [propget] HRESULT StrokeDashArray(); + [propget] HRESULT StrokeDashCap(); + [propput] HRESULT StrokeDashCap(); + [propget] HRESULT StrokeDashOffset(); + [propput] HRESULT StrokeDashOffset(); + [propget] HRESULT StrokeEndCap(); + [propput] HRESULT StrokeEndCap(); + [propget] HRESULT StrokeLineJoin(); + [propput] HRESULT StrokeLineJoin(); + [propget] HRESULT StrokeMiterLimit(); + [propput] HRESULT StrokeMiterLimit(); + [propget] HRESULT StrokeStartCap(); + [propput] HRESULT StrokeStartCap(); + [propget] HRESULT StrokeThickness(); + [propput] HRESULT StrokeThickness(); +} + +[contract(Windows.Foundation.UniversalApiContract, 6.0)] +[exclusiveto(Windows.UI.Composition.CompositionShape)] +[uuid(B47CE2F7-9A88-42C4-9E87-2E500CA8688C)] +interface ICompositionShape : IInspectable +{ + [propget] HRESULT CenterPoint([out] [retval] Vector2* value); + [propput] HRESULT CenterPoint([in] Vector2 value); +} + +[uuid(42d4219a-be1b-5091-8f1e-90270840fc2d)] +interface IVectorOfCompositionShape : IInspectable +{ + HRESULT GetAt(); + [propget] HRESULT GetSize(); + HRESULT GetView(); + HRESULT IndexOf(); + HRESULT SetAt(); + HRESULT InsertAt(); + HRESULT RemoveAt(); + HRESULT Append([in] ICompositionShape* value); + HRESULT RemoveAtEnd(); + HRESULT Clear(); +} + +[contract(Windows.Foundation.UniversalApiContract, 7.0)] +[exclusiveto(Windows.UI.Composition.CompositionGeometricClip)] +[uuid(C840B581-81C9-4444-A2C1-CCAECE3A50E5)] +interface ICompositionGeometricClip : IInspectable +{ + [propget] HRESULT Geometry([out] [retval] ICompositionGeometry** value); + [propput] HRESULT Geometry([in] ICompositionGeometry* value); +} + +[uuid(F2BD13C3-BA7E-4B0F-9126-FFB7536B8176)] +interface IShapeVisual : IInspectable +{ + [propget] HRESULT Shapes([out] [retval] IUnknown** value); +} + [uuid(48EA31AD-7FCD-4076-A79C-90CC4B852C9B)] interface ICompositor5 : IInspectable { @@ -709,10 +815,19 @@ interface ICompositor5 : IInspectable [overload("CreatePathGeometry")] HRESULT CreatePathGeometryWithPath([in] void* path, [out] [retval] void** result); HRESULT CreatePathKeyFrameAnimation([out] [retval] void** result); HRESULT CreateRectangleGeometry([out] [retval] void** result); - HRESULT CreateRoundedRectangleGeometry([out] [retval] void** result); - HRESULT CreateShapeVisual([out] [retval] void** result); + HRESULT CreateRoundedRectangleGeometry([out] [retval] ICompositionRoundedRectangleGeometry** result); + HRESULT CreateShapeVisual([out] [retval] IShapeVisual** result); [overload("CreateSpriteShape")] HRESULT CreateSpriteShape([out] [retval] void** result); [overload("CreateSpriteShape")] HRESULT CreateSpriteShapeWithGeometry([in] void* geometry, [out] [retval] void** result); HRESULT CreateViewBox([out] [retval] void** result); HRESULT RequestCommitAsync([out] [retval] IAsyncAction** operation); } + +[contract(Windows.Foundation.UniversalApiContract, 7.0)] +[exclusiveto(Windows.UI.Composition.Compositor)] +[uuid(7A38B2BD-CEC8-4EEB-830F-D8D07AEDEBC3)] +interface ICompositor6 : IInspectable +{ + [overload("CreateGeometricClip")] HRESULT CreateGeometricClip([out] [retval] ICompositionGeometricClip** result); + [overload("CreateGeometricClip")] HRESULT CreateGeometricClipWithGeometry([in] ICompositionGeometry* geometry, [out] [retval] ICompositionGeometricClip** result); +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 8fc25f8cfa..b7fc35db72 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -373,7 +373,13 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - _blurHost?.SetBlur(transparencyLevel >= WindowTransparencyLevel.Blur); + _blurHost?.SetBlur(transparencyLevel switch + { + WindowTransparencyLevel.Mica => BlurEffect.Mica, + WindowTransparencyLevel.AcrylicBlur => BlurEffect.Acrylic, + WindowTransparencyLevel.Blur => BlurEffect.Acrylic, + _ => BlurEffect.None + }); return transparencyLevel; } @@ -507,7 +513,7 @@ namespace Avalonia.Win32 public void Activate() { - SetActiveWindow(_hwnd); + SetForegroundWindow(_hwnd); } public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); @@ -1000,6 +1006,7 @@ namespace Avalonia.Win32 if (!Design.IsDesignMode && activate) { SetFocus(_hwnd); + SetForegroundWindow(_hwnd); } } diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs index c2526d7d9f..9c46aa78cc 100644 --- a/src/iOS/Avalonia.iOS/Stubs.cs +++ b/src/iOS/Avalonia.iOS/Stubs.cs @@ -21,6 +21,8 @@ namespace Avalonia.iOS public IWindowImpl CreateWindow() => throw new NotSupportedException(); public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); + + public ITrayIconImpl CreateTrayIcon() => null; } class PlatformIconLoaderStub : IPlatformIconLoader diff --git a/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs new file mode 100644 index 0000000000..321676abc0 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ItemsRepeaterTests.cs @@ -0,0 +1,24 @@ +using System.Collections.ObjectModel; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ItemsRepeaterTests + { + [Fact] + public void Can_Reassign_Items() + { + var target = new ItemsRepeater(); + target.Items = new ObservableCollection(); + target.Items = new ObservableCollection(); + } + + [Fact] + public void Can_Reassign_Items_To_Null() + { + var target = new ItemsRepeater(); + target.Items = new ObservableCollection(); + target.Items = null; + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index bf1322afbc..e8471d41fb 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -25,6 +25,11 @@ namespace Avalonia.Controls.UnitTests throw new NotImplementedException(); } + public ITrayIconImpl CreateTrayIcon() + { + return null; + } + public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs new file mode 100644 index 0000000000..9fee5285aa --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/GenericTemplateTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class SampleTemplatedObject : StyledElement + { + [Content] public List Content { get; set; } = new List(); + public string Foo { get; set; } + } + + public class SampleTemplatedObjectTemplate + { + [Content] + [TemplateContent(TemplateResultType = typeof(SampleTemplatedObject))] + public object Content { get; set; } + } + + public class SampleTemplatedObjectContainer + { + public SampleTemplatedObjectTemplate Template { get; set; } + } + + public class GenericTemplateTests + { + [Fact] + public void DataTemplate_Can_Be_Empty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var container = + (SampleTemplatedObjectContainer)AvaloniaRuntimeXamlLoader.Load(xaml, + typeof(GenericTemplateTests).Assembly); + var res = TemplateContent.Load(container.Template.Content); + Assert.Equal(res.Result, res.NameScope.Find("root")); + Assert.Equal(res.Result.Content[0], res.NameScope.Find("child1")); + Assert.Equal(res.Result.Content[1], res.NameScope.Find("child2")); + Assert.Equal("foo", res.Result.Content[0].Foo); + Assert.Equal("bar", res.Result.Content[1].Foo); + } + } + } +} 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/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs new file mode 100644 index 0000000000..56f2b03932 --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class LineNodeTests + { + [Fact] + public void HitTest_Should_Be_True() + { + var lineNode = new LineNode( + Matrix.Identity, + new Pen(Brushes.Black, 3), + new Point(15, 10), + new Point(150, 73)); + + var pointsInside = new List() + { + new Point(14, 8.9), + new Point(15, 10), + new Point(30, 15.5), + new Point(30, 18.5), + new Point(150, 73), + new Point(151, 71.9), + }; + + foreach (var point in pointsInside) + { + Assert.True(lineNode.HitTest(point)); + } + } + + [Fact] + public void HitTest_Should_Be_False() + { + var lineNode = new LineNode( + Matrix.Identity, + new Pen(Brushes.Black, 3), + new Point(15, 10), + new Point(150, 73)); + + var pointsOutside = new List() + { + new Point(14, 8), + new Point(14, 8.8), + new Point(30, 15.3), + new Point(30, 18.7), + new Point(151, 71.8), + new Point(155, 75), + }; + + foreach (var point in pointsOutside) + { + Assert.False(lineNode.HitTest(point)); + } + } + } +}