Browse Source

Merge pull request #6560 from AvaloniaUI/feature/tray-icon-support

Feature/tray icon support
# Conflicts:
#	src/Avalonia.Controls/ApiCompatBaseline.txt
release/0.10.8
Dan Walmsley 4 years ago
parent
commit
081c89c9ec
  1. 6
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  2. 1
      native/Avalonia.Native/src/OSX/common.h
  3. 11
      native/Avalonia.Native/src/OSX/main.mm
  4. 33
      native/Avalonia.Native/src/OSX/trayicon.h
  5. 85
      native/Avalonia.Native/src/OSX/trayicon.mm
  6. 25
      samples/ControlCatalog/App.xaml
  7. 9
      samples/ControlCatalog/App.xaml.cs
  8. 2
      samples/ControlCatalog/MainWindow.xaml.cs
  9. 26
      samples/ControlCatalog/ViewModels/ApplicationViewModel.cs
  10. 5
      src/Avalonia.Base/Logging/LogArea.cs
  11. 4
      src/Avalonia.Controls/ApiCompatBaseline.txt
  12. 13
      src/Avalonia.Controls/NativeMenu.Export.cs
  13. 17
      src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs
  14. 35
      src/Avalonia.Controls/Platform/ITrayIconImpl.cs
  15. 5
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  16. 16
      src/Avalonia.Controls/Platform/PlatformManager.cs
  17. 187
      src/Avalonia.Controls/TrayIcon.cs
  18. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  19. 7
      src/Avalonia.FreeDesktop/DBusHelper.cs
  20. 75
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  21. 2
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  22. 63
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  23. 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  24. 63
      src/Avalonia.Native/TrayIconImpl.cs
  25. 9
      src/Avalonia.Native/avn.idl
  26. 6
      src/Avalonia.X11/X11Platform.cs
  27. 367
      src/Avalonia.X11/X11TrayIconImpl.cs
  28. 2
      src/Avalonia.X11/X11Window.cs
  29. 63
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  30. 265
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  31. 54
      src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs
  32. 11
      src/Windows/Avalonia.Win32/Win32Platform.cs
  33. 3
      src/Windows/Avalonia.Win32/WindowImpl.cs
  34. 2
      src/iOS/Avalonia.iOS/Stubs.cs
  35. 5
      tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs
  36. 5
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

6
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 */; }; 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; }; 520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; }; 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 */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.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 = "<group>"; }; 37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = "<group>"; };
520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = "<group>"; }; 520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = "<group>"; };
522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; 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 = "<group>"; };
523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = "<group>"; };
5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = "<group>"; }; 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = "<group>"; };
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
@ -114,6 +117,8 @@
AB00E4F62147CA920032A60A /* main.mm */, AB00E4F62147CA920032A60A /* main.mm */,
37155CE3233C00EB0034DCE9 /* menu.h */, 37155CE3233C00EB0034DCE9 /* menu.h */,
520624B222973F4100C4DCEF /* menu.mm */, 520624B222973F4100C4DCEF /* menu.mm */,
523484C926EA688F00EA0C2C /* trayicon.mm */,
523484CB26EA68AA00EA0C2C /* trayicon.h */,
1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */, 1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
37A517B22159597E00FBA241 /* Screens.mm */, 37A517B22159597E00FBA241 /* Screens.mm */,
37C09D8721580FE4006A6758 /* SystemDialogs.mm */, 37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
@ -204,6 +209,7 @@
1A1852DC23E05814008F0DED /* deadlock.mm in Sources */, 1A1852DC23E05814008F0DED /* deadlock.mm in Sources */,
5B21A982216530F500CEE36E /* cursor.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */,
37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */,
AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */, AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */, 1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */,
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */, 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */,

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

@ -22,6 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop);
extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay(); extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
extern IAvnTrayIcon* CreateTrayIcon();
extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItem();
extern IAvnMenuItem* CreateAppMenuItemSeparator(); extern IAvnMenuItem* CreateAppMenuItemSeparator();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);

11
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 virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override
{ {
START_COM_CALL; START_COM_CALL;

33
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<IAvnTrayIcon, &IID_IAvnTrayIcon>
{
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 */

85
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<AvnAppMenu*>(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;
}

25
samples/ControlCatalog/App.xaml

@ -1,5 +1,8 @@
<Application xmlns="https://github.com/avaloniaui" <Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ControlCatalog.ViewModels"
x:DataType="vm:ApplicationViewModel"
x:CompileBindings="True"
x:Class="ControlCatalog.App"> x:Class="ControlCatalog.App">
<Application.Styles> <Application.Styles>
<Style Selector="TextBlock.h1"> <Style Selector="TextBlock.h1">
@ -22,6 +25,26 @@
<Style Selector="Label.h3"> <Style Selector="Label.h3">
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="12" />
</Style> </Style>
<StyleInclude Source="/SideBar.xaml"/> <StyleInclude Source="/SideBar.xaml" />
</Application.Styles> </Application.Styles>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/test_icon.ico" ToolTipText="Avalonia Tray Icon ToolTip">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="Settings">
<NativeMenu>
<NativeMenuItem Header="Option 1" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Header="Option 2" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
<NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
</NativeMenu>
</NativeMenuItem>
<NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />
</NativeMenu>
</TrayIcon.Menu>
</TrayIcon>
</TrayIcons>
</TrayIcon.Icons>
</Application> </Application>

9
samples/ControlCatalog/App.xaml.cs

@ -1,14 +1,21 @@
using System; using System;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Styling;
using Avalonia.Styling; using Avalonia.Styling;
using ControlCatalog.ViewModels;
namespace ControlCatalog namespace ControlCatalog
{ {
public class App : Application public class App : Application
{ {
public App()
{
DataContext = new ApplicationViewModel();
}
private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{ {
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
@ -97,7 +104,9 @@ namespace ControlCatalog
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow(); desktopLifetime.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
singleViewLifetime.MainView = new MainView(); singleViewLifetime.MainView = new MainView();

2
samples/ControlCatalog/MainWindow.xaml.cs

@ -35,6 +35,8 @@ namespace ControlCatalog
var mainMenu = this.FindControl<Menu>("MainMenu"); var mainMenu = this.FindControl<Menu>("MainMenu");
mainMenu.AttachedToVisualTree += MenuAttached; mainMenu.AttachedToVisualTree += MenuAttached;
ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar;
} }
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit"; public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";

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

5
src/Avalonia.Base/Logging/LogArea.cs

@ -39,5 +39,10 @@ namespace Avalonia.Logging
/// The log event comes from Win32Platform. /// The log event comes from Win32Platform.
/// </summary> /// </summary>
public const string Win32Platform = nameof(Win32Platform); public const string Win32Platform = nameof(Win32Platform);
/// <summary>
/// The log event comes from X11Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
} }
} }

4
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -8,6 +8,7 @@ MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
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. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
@ -26,4 +27,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. 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. 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. 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: 27 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 58

13
src/Avalonia.Controls/NativeMenu.Export.cs

@ -52,15 +52,10 @@ namespace Avalonia.Controls
} }
public static readonly AttachedProperty<NativeMenu> MenuProperty public static readonly AttachedProperty<NativeMenu> MenuProperty
= AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu"/*, validate: = AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu");
(o, v) =>
{
if(!(o is Application || o is TopLevel))
throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType());
return v;
}*/);
public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu); public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu);
public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty); public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty);
static NativeMenu() static NativeMenu()
@ -79,6 +74,10 @@ namespace Avalonia.Controls
{ {
GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault()); GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
} }
else if(args.Sender is INativeMenuExporterProvider provider)
{
provider.NativeMenuExporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
}
}); });
} }
} }

17
src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs

@ -1,14 +1,25 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Platform; using Avalonia.Platform;
#nullable enable
namespace Avalonia.Controls.Platform namespace Avalonia.Controls.Platform
{ {
public interface ITopLevelNativeMenuExporter public interface INativeMenuExporter
{
void SetNativeMenu(NativeMenu? menu);
}
public interface ITopLevelNativeMenuExporter : INativeMenuExporter
{ {
bool IsNativeMenuExported { get; } bool IsNativeMenuExported { get; }
event EventHandler OnIsNativeMenuExportedChanged; event EventHandler OnIsNativeMenuExportedChanged;
void SetNativeMenu(NativeMenu menu); }
public interface INativeMenuExporterProvider
{
INativeMenuExporter? NativeMenuExporter { get; }
} }
public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl

35
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
{
/// <summary>
/// Sets the icon of this tray icon.
/// </summary>
void SetIcon(IWindowIconImpl? icon);
/// <summary>
/// Sets the icon of this tray icon.
/// </summary>
void SetToolTipText(string? text);
/// <summary>
/// Sets if the tray icon is visible or not.
/// </summary>
void SetIsVisible(bool visible);
/// <summary>
/// Gets the MenuExporter to allow native menus to be exported to the TrayIcon.
/// </summary>
INativeMenuExporter? MenuExporter { get; }
/// <summary>
/// Gets or Sets the Action that is called when the TrayIcon is clicked.
/// </summary>
Action? OnClicked { get; set; }
}
}

5
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@ -1,8 +1,13 @@
#nullable enable
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
public interface IWindowingPlatform public interface IWindowingPlatform
{ {
IWindowImpl CreateWindow(); IWindowImpl CreateWindow();
IWindowImpl CreateEmbeddableWindow(); IWindowImpl CreateEmbeddableWindow();
ITrayIconImpl? CreateTrayIcon();
} }
} }

16
src/Avalonia.Controls/Platform/PlatformManager.cs

@ -1,8 +1,9 @@
using System; using System;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
#nullable enable
namespace Avalonia.Controls.Platform namespace Avalonia.Controls.Platform
{ {
public static partial class PlatformManager public static partial class PlatformManager
@ -22,6 +23,19 @@ namespace Avalonia.Controls.Platform
{ {
} }
public static ITrayIconImpl? CreateTrayIcon()
{
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
if (platform == null)
{
throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered.");
}
return s_designerMode ? null : platform.CreateTrayIcon();
}
public static IWindowImpl CreateWindow() public static IWindowImpl CreateWindow()
{ {
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>(); var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();

187
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<TrayIcon>
{
}
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;
}
}
/// <summary>
/// Raised when the TrayIcon is clicked.
/// Note, this is only supported on Win32 and some Linux DEs,
/// on OSX this event is not raised.
/// </summary>
public event EventHandler? Clicked;
/// <summary>
/// Defines the <see cref="TrayIcons"/> attached property.
/// </summary>
public static readonly AttachedProperty<TrayIcons> IconsProperty
= AvaloniaProperty.RegisterAttached<TrayIcon, Application, TrayIcons>("Icons");
/// <summary>
/// Defines the <see cref="Menu"/> property.
/// </summary>
public static readonly StyledProperty<NativeMenu?> MenuProperty
= AvaloniaProperty.Register<TrayIcon, NativeMenu?>(nameof(Menu));
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
public static readonly StyledProperty<WindowIcon> IconProperty =
Window.IconProperty.AddOwner<TrayIcon>();
/// <summary>
/// Defines the <see cref="ToolTipText"/> property.
/// </summary>
public static readonly StyledProperty<string?> ToolTipTextProperty =
AvaloniaProperty.Register<TrayIcon, string?>(nameof(ToolTipText));
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVisibleProperty =
Visual.IsVisibleProperty.AddOwner<TrayIcon>();
public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty);
/// <summary>
/// Gets or sets the Menu of the TrayIcon.
/// </summary>
public NativeMenu? Menu
{
get => GetValue(MenuProperty);
set => SetValue(MenuProperty, value);
}
/// <summary>
/// Gets or sets the icon of the TrayIcon.
/// </summary>
public WindowIcon Icon
{
get => GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
/// <summary>
/// Gets or sets the tooltip text of the TrayIcon.
/// </summary>
public string? ToolTipText
{
get => GetValue(ToolTipTextProperty);
set => SetValue(ToolTipTextProperty, value);
}
/// <summary>
/// Gets or sets the visibility of the TrayIcon.
/// </summary>
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<TrayIcon>());
}
private static void RemoveIcons(IEnumerable<TrayIcon> icons)
{
foreach (var icon in icons)
{
icon.Dispose();
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == IconProperty)
{
_impl?.SetIcon(Icon.PlatformImpl);
}
else if (change.Property == IsVisibleProperty)
{
_impl?.SetIsVisible(change.NewValue.GetValueOrDefault<bool>());
}
else if (change.Property == ToolTipTextProperty)
{
_impl?.SetToolTipText(change.NewValue.GetValueOrDefault<string?>());
}
else if (change.Property == MenuProperty)
{
_impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault<NativeMenu>());
}
}
/// <summary>
/// Disposes the tray icon (removing it from the tray area).
/// </summary>
public void Dispose() => _impl?.Dispose();
}
}

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

@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote
private static DetachableTransportConnection s_lastWindowTransport; private static DetachableTransportConnection s_lastWindowTransport;
private static PreviewerWindowImpl s_lastWindow; private static PreviewerWindowImpl s_lastWindow;
public static List<object> PreFlightMessages = new List<object>(); public static List<object> PreFlightMessages = new List<object>();
public ITrayIconImpl CreateTrayIcon() => null;
public IWindowImpl CreateWindow() => new WindowStub(); public IWindowImpl CreateWindow() => new WindowStub();
public IWindowImpl CreateEmbeddableWindow() public IWindowImpl CreateEmbeddableWindow()

7
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop
public static Connection TryInitialize(string dbusAddress = null) public static Connection TryInitialize(string dbusAddress = null)
{ {
if (Connection != null) return Connection ?? TryGetConnection(dbusAddress);
return Connection; }
public static Connection TryGetConnection(string dbusAddress = null)
{
var oldContext = SynchronizationContext.Current; var oldContext = SynchronizationContext.Current;
try try
{ {

75
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -8,6 +8,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop.DBusMenu; using Avalonia.FreeDesktop.DBusMenu;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using Tmds.DBus; using Tmds.DBus;
#pragma warning disable 1998 #pragma warning disable 1998
@ -16,51 +17,78 @@ namespace Avalonia.FreeDesktop
{ {
public class DBusMenuExporter public class DBusMenuExporter
{ {
public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid)
{ {
if (DBusHelper.Connection == null) if (DBusHelper.Connection == null)
return null; return null;
return new DBusMenuExporterImpl(DBusHelper.Connection, xid); 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 Connection _dbus;
private readonly uint _xid; private readonly uint _xid;
private IRegistrar _registar; private IRegistrar _registrar;
private bool _disposed; private bool _disposed;
private uint _revision = 1; private uint _revision = 1;
private NativeMenu _menu; private NativeMenu _menu;
private Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>(); private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>(); private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>(); private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
private bool _resetQueued; private bool _resetQueued;
private int _nextId = 1; private int _nextId = 1;
private bool _appMenu = true;
public DBusMenuExporterImpl(Connection dbus, IntPtr xid) public DBusMenuExporterImpl(Connection dbus, IntPtr xid)
{ {
_dbus = dbus; _dbus = dbus;
_xid = (uint)xid.ToInt32(); _xid = (uint)xid.ToInt32();
ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" ObjectPath = GenerateDBusMenuObjPath;
+ Guid.NewGuid().ToString().Replace("-", ""));
SetNativeMenu(new NativeMenu()); SetNativeMenu(new NativeMenu());
Init(); Init();
} }
public DBusMenuExporterImpl(Connection dbus, ObjectPath path)
{
_dbus = dbus;
_appMenu = false;
ObjectPath = path;
SetNativeMenu(new NativeMenu());
Init();
}
async void Init() async void Init()
{ {
try try
{ {
await _dbus.RegisterObjectAsync(this); if (_appMenu)
_registar = DBusHelper.Connection.CreateProxy<IRegistrar>( {
"com.canonical.AppMenu.Registrar", await _dbus.RegisterObjectAsync(this);
"/com/canonical/AppMenu/Registrar"); _registrar = DBusHelper.Connection.CreateProxy<IRegistrar>(
if (!_disposed) "com.canonical.AppMenu.Registrar",
await _registar.RegisterWindowAsync(_xid, ObjectPath); "/com/canonical/AppMenu/Registrar");
if (!_disposed)
await _registrar.RegisterWindowAsync(_xid, ObjectPath);
}
else
{
await _dbus.RegisterObjectAsync(this);
}
} }
catch (Exception e) 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, // It's not really important if this code succeeds,
// and it's not important to know if it succeeds // and it's not important to know if it succeeds
// since even if we register the window it's not guaranteed that // since even if we register the window it's not guaranteed that
@ -75,7 +103,7 @@ namespace Avalonia.FreeDesktop
_disposed = true; _disposed = true;
_dbus.UnregisterObject(this); _dbus.UnregisterObject(this);
// Fire and forget // Fire and forget
_registar?.UnregisterWindowAsync(_xid); _registrar?.UnregisterWindowAsync(_xid);
} }
@ -248,17 +276,24 @@ namespace Avalonia.FreeDesktop
if (item.ToggleType != NativeMenuItemToggleType.None) if (item.ToggleType != NativeMenuItemToggleType.None)
return item.IsChecked ? 1 : 0; return item.IsChecked ? 1 : 0;
} }
if (name == "icon-data") if (name == "icon-data")
{ {
if (item.Icon != null) if (item.Icon != null)
{ {
var ms = new MemoryStream(); var loader = AvaloniaLocator.Current.GetService<IPlatformIconLoader>();
item.Icon.Save(ms);
return ms.ToArray(); 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") if (name == "children-display")
return menu != null ? "submenu" : null; return menu != null ? "submenu" : null;
} }

2
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -51,6 +51,8 @@ namespace Avalonia.Headless
public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
public ITrayIconImpl CreateTrayIcon() => null;
} }
internal static void Initialize() internal static void Initialize()

63
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -9,14 +9,15 @@ using Avalonia.Threading;
namespace Avalonia.Native namespace Avalonia.Native
{ {
class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
{ {
private IAvaloniaNativeFactory _factory; private readonly IAvaloniaNativeFactory _factory;
private bool _resetQueued = true; private bool _resetQueued = true;
private bool _exported = false; private bool _exported;
private IAvnWindow _nativeWindow; private readonly IAvnWindow _nativeWindow;
private NativeMenu _menu; private NativeMenu _menu;
private __MicroComIAvnMenuProxy _nativeMenu; private __MicroComIAvnMenuProxy _nativeMenu;
private readonly IAvnTrayIcon _trayIcon;
public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
{ {
@ -33,13 +34,21 @@ namespace Avalonia.Native
DoLayoutReset(); DoLayoutReset();
} }
public AvaloniaNativeMenuExporter(IAvnTrayIcon trayIcon, IAvaloniaNativeFactory factory)
{
_factory = factory;
_trayIcon = trayIcon;
DoLayoutReset();
}
public bool IsNativeMenuExported => _exported; public bool IsNativeMenuExported => _exported;
public event EventHandler OnIsNativeMenuExportedChanged; public event EventHandler OnIsNativeMenuExportedChanged;
public void SetNativeMenu(NativeMenu menu) public void SetNativeMenu(NativeMenu menu)
{ {
_menu = menu == null ? new NativeMenu() : menu; _menu = menu ?? new NativeMenu();
DoLayoutReset(true); DoLayoutReset(true);
} }
@ -82,15 +91,22 @@ namespace Avalonia.Native
if (_nativeWindow is null) 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(); SetMenu(_trayIcon, _menu);
NativeMenu.SetMenu(Application.Current, appMenu);
} }
SetMenu(appMenu);
} }
else else
{ {
@ -118,7 +134,7 @@ namespace Avalonia.Native
var appMenuHolder = menuItem?.Parent; var appMenuHolder = menuItem?.Parent;
if (menu.Parent is null) if (menuItem is null)
{ {
menuItem = new NativeMenuItem(); menuItem = new NativeMenuItem();
} }
@ -136,7 +152,7 @@ namespace Avalonia.Native
if (_nativeMenu is null) if (_nativeMenu is null)
{ {
_nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory); _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory);
_nativeMenu.Initialize(this, appMenuHolder, ""); _nativeMenu.Initialize(this, appMenuHolder, "");
@ -171,5 +187,26 @@ namespace Avalonia.Native
avnWindow.SetMainMenu(_nativeMenu); 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);
}
}
} }
} }

5
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -134,6 +134,11 @@ namespace Avalonia.Native
} }
} }
public ITrayIconImpl CreateTrayIcon ()
{
return new TrayIconImpl(_factory);
}
public IWindowImpl CreateWindow() public IWindowImpl CreateWindow()
{ {
return new WindowImpl(_factory, _options, _platformGl); return new WindowImpl(_factory, _options, _platformGl);

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

9
src/Avalonia.Native/avn.idl

@ -427,6 +427,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv); HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
HRESULT CreateMenuItem(IAvnMenuItem** ppv); HRESULT CreateMenuItem(IAvnMenuItem** ppv);
HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv); HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
HRESULT CreateTrayIcon(IAvnTrayIcon** ppv);
} }
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -665,6 +666,14 @@ interface IAvnGlSurfaceRenderingSession : IUnknown
HRESULT GetScaling(double* ret); 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)] [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)]
interface IAvnMenu : IUnknown interface IAvnMenu : IUnknown
{ {

6
src/Avalonia.X11/X11Platform.cs

@ -100,6 +100,12 @@ namespace Avalonia.X11
public IntPtr DeferredDisplay { get; set; } public IntPtr DeferredDisplay { get; set; }
public IntPtr Display { get; set; } public IntPtr Display { get; set; }
public ITrayIconImpl CreateTrayIcon ()
{
return new X11TrayIconImpl();
}
public IWindowImpl CreateWindow() public IWindowImpl CreateWindow()
{ {
return new X11Window(this, null); return new X11Window(this, null);

367
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<IStatusNotifierWatcher>(
"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);
}
}
/// <summary>
/// DBus Object used for setting system tray icons.
/// </summary>
/// <remarks>
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
/// </remarks>
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<string>? 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<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError)
{
OnTitleChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler));
}
public Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError)
{
OnIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler));
}
public Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError)
{
OnAttentionIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler));
}
public Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError)
{
OnOverlayIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler));
}
public Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError)
{
OnTooltipChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler));
}
public Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError)
{
NewStatusAsync += handler;
return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
}
public Task<object> GetAsync(string prop) => Task.FromResult(new object());
public Task<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);
public Task SetAsync(string prop, object val) => Task.CompletedTask;
public Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> 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<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError);
Task<object> GetAsync(string prop);
Task<StatusNotifierItemProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> 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<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>())
};
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;
}
}
}

2
src/Avalonia.X11/X11Window.cs

@ -190,7 +190,7 @@ namespace Avalonia.X11
if(_popup) if(_popup)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
if (platform.Options.UseDBusMenu) if (platform.Options.UseDBusMenu)
NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle);
NativeControlHost = new X11NativeControlHost(_platform, this); NativeControlHost = new X11NativeControlHost(_platform, this);
InitializeIme(); InitializeIme();
} }

63
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1110,6 +1110,9 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetActiveWindow(IntPtr hWnd); public static extern IntPtr SetActiveWindow(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr SetCapture(IntPtr hWnd); public static extern IntPtr SetCapture(IntPtr hWnd);
@ -1197,6 +1200,9 @@ namespace Avalonia.Win32.Interop
GCW_ATOM = -32 GCW_ATOM = -32
} }
[DllImport("shell32", CharSet = CharSet.Auto)]
public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData);
[DllImport("user32.dll", EntryPoint = "SetClassLongPtr")] [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")]
private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong);
@ -2296,4 +2302,61 @@ namespace Avalonia.Win32.Interop
public uint VisibleMask; public uint VisibleMask;
public uint DamageMask; 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<NOTIFYICONDATA>();
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;
}
} }

265
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<int, TrayIconImpl> s_trayIcons = new Dictionary<int, TrayIconImpl>();
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();
}
/// <summary>
/// Custom Win32 window messages for the NotifyIcon
/// </summary>
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<TrayPopupRoot>();
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<PixelPoint, Size, double> _moveResize;
private readonly Window _hiddenWindow;
public TrayIconManagedPopupPositionerPopupImplHelper(Action<PixelPoint, Size, double> moveResize)
{
_moveResize = moveResize;
_hiddenWindow = new Window();
}
public IReadOnlyList<ManagedPopupPositionerScreenInfo> 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);
}
}
}

54
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<MenuItem> 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<MenuItem>? GetMenu()
{
if (_nativeMenu != null)
{
return Populate(_nativeMenu);
}
return null;
}
}
}

11
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -108,6 +108,10 @@ namespace Avalonia.Win32
CreateMessageWindow(); CreateMessageWindow();
} }
internal static Win32Platform Instance => s_instance;
internal IntPtr Handle => _hwnd;
/// <summary> /// <summary>
/// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion. /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion.
/// </summary> /// </summary>
@ -261,6 +265,8 @@ namespace Avalonia.Win32
} }
} }
} }
TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam);
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
} }
@ -293,6 +299,11 @@ namespace Avalonia.Win32
} }
} }
public ITrayIconImpl CreateTrayIcon ()
{
return new TrayIconImpl();
}
public IWindowImpl CreateWindow() public IWindowImpl CreateWindow()
{ {
return new WindowImpl(); return new WindowImpl();

3
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -507,7 +507,7 @@ namespace Avalonia.Win32
public void Activate() public void Activate()
{ {
SetActiveWindow(_hwnd); SetForegroundWindow(_hwnd);
} }
public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this); public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this);
@ -1000,6 +1000,7 @@ namespace Avalonia.Win32
if (!Design.IsDesignMode && activate) if (!Design.IsDesignMode && activate)
{ {
SetFocus(_hwnd); SetFocus(_hwnd);
SetForegroundWindow(_hwnd);
} }
} }

2
src/iOS/Avalonia.iOS/Stubs.cs

@ -21,6 +21,8 @@ namespace Avalonia.iOS
public IWindowImpl CreateWindow() => throw new NotSupportedException(); public IWindowImpl CreateWindow() => throw new NotSupportedException();
public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException(); public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException();
public ITrayIconImpl CreateTrayIcon() => null;
} }
class PlatformIconLoaderStub : IPlatformIconLoader class PlatformIconLoaderStub : IPlatformIconLoader

5
tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs

@ -25,6 +25,11 @@ namespace Avalonia.Controls.UnitTests
throw new NotImplementedException(); throw new NotImplementedException();
} }
public ITrayIconImpl CreateTrayIcon()
{
return null;
}
public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of<IPopupImpl>(x => x.RenderScaling == 1); public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of<IPopupImpl>(x => x.RenderScaling == 1);
} }
} }

5
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -126,6 +126,11 @@ namespace Avalonia.UnitTests
throw new NotImplementedException(); throw new NotImplementedException();
} }
public ITrayIconImpl CreateTrayIcon()
{
return null;
}
private static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl private static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
{ {
mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());

Loading…
Cancel
Save