From 141e749226c95557f4b44347525abbe3fa2db230 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:04:09 +0800 Subject: [PATCH 01/26] Initial Commit for handling DBus SNI Tray Icons gracefully and also making a skeleton class for the future XEmbed Tray Icon impl. --- .../DbusSNITrayIconImpl.cs | 358 +++++++++++++++++ src/Avalonia.X11/X11TrayIconImpl.cs | 379 ++++-------------- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 36 ++ 3 files changed, 462 insertions(+), 311 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs create mode 100644 src/Avalonia.X11/XEmbedTrayIconImpl.cs diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs new file mode 100644 index 0000000000..1fb74f132a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -0,0 +1,358 @@ +#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.FreeDesktop +{ + public class DbusSNITrayIconImpl + { + 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 bool IsActive => _isActive; + + public DbusSNITrayIconImpl(Connection connection) + { + _connection = connection; + _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(UIntPtr[] x11iconData) + { + if (_isDisposed) + return; + var w = (int)x11iconData[0]; + var h = (int)x11iconData[1]; + + var pixLength = w * h; + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; + + for (var i = 0; i < pixLength; i++) + { + var rawPixel = x11iconData[i + 2].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/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 371ff75408..ca8ed8ec35 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,367 +1,124 @@ -#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(); + _xEmbedTrayIcon = new XEmbedTrayIconImpl(); + + var _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; + _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); } - 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); + private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; - } - - public async void DestroyTrayIcon() - { - if (_connection is null) - return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; - } + private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; + private bool _isDisposed; public void Dispose() { + _dbusSniTrayIcon?.Dispose(); + _xEmbedTrayIcon?.Dispose(); _isDisposed = true; - DestroyTrayIcon(); - _connection?.Dispose(); } public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) - return; - if (!(icon is X11IconData x11icon)) - return; + if (_isDisposed) 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]; + if (_dbusSniTrayIcon?.IsActive ?? false) + { + if (!(icon is X11IconData x11icon)) + return; - for (var i = 0; i < pixLength; i++) + _dbusSniTrayIcon.SetIcon(x11icon.Data); + } + else { - 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); + _xEmbedTrayIcon.SetIcon(icon); } - - _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj?.SetIcon(_icon); } - public void SetIsVisible(bool visible) + public void SetToolTipText(string? text) { - if (_isDisposed || !_ctorFinished) - return; + if (_isDisposed) return; - if (visible & !_isActive) + if (_dbusSniTrayIcon?.IsActive ?? false) { - DestroyTrayIcon(); - CreateTrayIcon(); + _dbusSniTrayIcon.SetToolTipText(text); } - else if (!visible & _isActive) + else { - DestroyTrayIcon(); + _xEmbedTrayIcon.SetToolTipText(text); } } - 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) + public void SetIsVisible(bool visible) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + if (_isDisposed) return; - _backingProperties = new StatusNotifierItemProperties + if (_dbusSniTrayIcon?.IsActive ?? false) { - 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, "") - { + _dbusSniTrayIcon.SetIsVisible(visible); + } + else + { + _xEmbedTrayIcon.SetIsVisible(visible); + } } - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + public INativeMenuExporter? MenuExporter { - First = first; - Second = second; - Third = third; - Fourth = fourth; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.MenuExporter; + } + else + { + return _xEmbedTrayIcon.MenuExporter; + } + } } - } - - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - public DbusPixmap(int width, int height, byte[] data) + public Action? OnClicked { - Width = width; - Height = height; - Data = data; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.OnClicked; + } + else + { + return _xEmbedTrayIcon.OnClicked; + } + } + set + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + _dbusSniTrayIcon.OnClicked = value; + } + else + { + _xEmbedTrayIcon.OnClicked = value; + } + } } } } diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs new file mode 100644 index 0000000000..4b5f0d0a57 --- /dev/null +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Logging; +using Avalonia.Platform; + +namespace Avalonia.X11 +{ + internal class XEmbedTrayIconImpl + { + public XEmbedTrayIconImpl() + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, + "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); + } + + public void Dispose() + { + } + + public void SetIcon(IWindowIconImpl? icon) + { + } + + public void SetToolTipText(string? text) + { + } + + public void SetIsVisible(bool visible) + { + } + + public INativeMenuExporter? MenuExporter { get; } + public Action? OnClicked { get; set; } + } +} From c4b0b99027491c78988a81559244eddc99151e4c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 15:05:35 +0800 Subject: [PATCH 02/26] Gracefully handle tray service restarts --- .../DbusSNITrayIconImpl.cs | 129 +++++++++++++----- src/Avalonia.X11/X11TrayIconImpl.cs | 11 +- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 16 ++- 3 files changed, 113 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs index 1fb74f132a..6ca05efe50 100644 --- a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -6,49 +6,55 @@ 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.FreeDesktop { - public class DbusSNITrayIconImpl + public class DbusSNITrayIconImpl { - private static int s_trayIconInstanceId; + private static int s_trayIconInstanceId = 0; 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; + private bool _serviceConnected; + private readonly IDisposable _serviceWatchDisposable; + private bool _isVisible; public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public bool IsActive => _isActive; - - public DbusSNITrayIconImpl(Connection connection) + public bool IsActive => _serviceConnected; + + public DbusSNITrayIconImpl() { - _connection = connection; + _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); + InitializeSNWService(); CreateTrayIcon(); - _ctorFinished = true; + _serviceWatchDisposable = Watch(); } - public async void CreateTrayIcon() + private void InitializeSNWService() { - if (_connection is null) - return; + if (_connection is null || _isDisposed) return; try { @@ -61,38 +67,83 @@ namespace Avalonia.FreeDesktop 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."); + + return; } - if (_statusNotifierWatcher is null) + _serviceConnected = true; + } + + + private async Task Watch() => + await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + + + private void OnNameChange(ServiceOwnerChangedEventArgs obj) + { + if (_isDisposed) return; + if (!_serviceConnected & obj.NewOwner != null) + { + _serviceConnected = true; + + if (_isVisible) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else + { + DestroyTrayIcon(); + } + } + else if (_serviceConnected & obj.NewOwner is null) + { + s_trayIconInstanceId = 0; + _serviceConnected = false; + } + } + + public void CreateTrayIcon() + { + if (_connection is null || !_serviceConnected || _isDisposed) + return; + + var pid = Process.GetCurrentProcess().Id; var tid = s_trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await _connection.RegisterServiceAsync(_sysTrayServiceName); - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + try + { + _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + _connection.RegisterServiceAsync(_sysTrayServiceName); + _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + } + catch (Exception e) + { + _serviceConnected = false; + } _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; + _isVisible = true; } - public async void DestroyTrayIcon() + public void DestroyTrayIcon() { - if (_connection is null) + if (_connection is null || !_serviceConnected || _isDisposed) return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; + _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isVisible = false; } public void Dispose() @@ -100,12 +151,13 @@ namespace Avalonia.FreeDesktop _isDisposed = true; DestroyTrayIcon(); _connection?.Dispose(); + _serviceWatchDisposable?.Dispose(); } public void SetIcon(UIntPtr[] x11iconData) { if (_isDisposed) - return; + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -128,15 +180,15 @@ namespace Avalonia.FreeDesktop public void SetIsVisible(bool visible) { - if (_isDisposed || !_ctorFinished) + if (_isDisposed) return; - if (visible & !_isActive) + if (visible && !_isVisible) { DestroyTrayIcon(); CreateTrayIcon(); } - else if (!visible & _isActive) + else if (!visible && _isVisible) { DestroyTrayIcon(); } @@ -239,7 +291,20 @@ namespace Avalonia.FreeDesktop return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public Task GetAsync(string prop) => Task.FromResult(new object()); + public async Task GetAsync(string prop) + { + return prop switch + { + nameof(_backingProperties.Category) => _backingProperties.Category, + nameof(_backingProperties.Id) => _backingProperties.Id, + nameof(_backingProperties.Menu) => _backingProperties.Menu, + nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, + nameof(_backingProperties.Status) => _backingProperties.Status, + nameof(_backingProperties.Title) => _backingProperties.Title, + nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, + _ => null + }; + } public Task GetAllAsync() => Task.FromResult(_backingProperties); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index ca8ed8ec35..9e03dcd604 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -11,16 +11,7 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - - var _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."); - } - - _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); + _dbusSniTrayIcon = new DbusSNITrayIconImpl(); } private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index 4b5f0d0a57..c2247565be 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -9,25 +9,39 @@ namespace Avalonia.X11 { public XEmbedTrayIconImpl() { + } + + private bool IsCalled; + + private void NotImplemented() + { + if(IsCalled) return; + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); - } + IsCalled = true; + } + public void Dispose() { + NotImplemented(); } public void SetIcon(IWindowIconImpl? icon) { + NotImplemented(); } public void SetToolTipText(string? text) { + NotImplemented(); } public void SetIsVisible(bool visible) { + NotImplemented(); } public INativeMenuExporter? MenuExporter { get; } From 1302d3459a8863c347452b4e5fd1bbb704cb3ea4 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:13:17 +0800 Subject: [PATCH 03/26] Fix review comments --- src/Avalonia.FreeDesktop/DBusHelper.cs | 4 +- ...SNITrayIconImpl.cs => DBusTrayIconImpl.cs} | 44 +++++++++---------- src/Avalonia.X11/X11TrayIconImpl.cs | 30 ++++++------- 3 files changed, 39 insertions(+), 39 deletions(-) rename src/Avalonia.FreeDesktop/{DbusSNITrayIconImpl.cs => DBusTrayIconImpl.cs} (96%) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 4e23711ed4..b5bfb8b116 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -51,10 +51,10 @@ namespace Avalonia.FreeDesktop public static Connection TryInitialize(string dbusAddress = null) { - return Connection ?? TryGetConnection(dbusAddress); + return Connection ?? TryCreateNewConnection(dbusAddress); } - public static Connection TryGetConnection(string dbusAddress = null) + public static Connection TryCreateNewConnection(string dbusAddress = null) { var oldContext = SynchronizationContext.Current; try diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs similarity index 96% rename from src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs rename to src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 6ca05efe50..2d30d2ee21 100644 --- a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -10,32 +10,34 @@ using Avalonia.Logging; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] - +[assembly: InternalsVisibleTo("Avalonia.X11")] namespace Avalonia.FreeDesktop { - public class DbusSNITrayIconImpl + internal class DBusTrayIconImpl { - private static int s_trayIconInstanceId = 0; + private static int s_trayIconInstanceId; + private readonly ObjectPath _dbusMenuPath; - private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; - private DbusPixmap _icon; + private readonly IDisposable _serviceWatchDisposable; + + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private IStatusNotifierWatcher? _statusNotifierWatcher; + private DbusPixmap _icon; + private string? _sysTrayServiceName; private string? _tooltipText; private bool _isDisposed; private bool _serviceConnected; - private readonly IDisposable _serviceWatchDisposable; - private bool _isVisible; - + private bool _isVisible = true; + + public bool IsActive => _serviceConnected; public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - - public bool IsActive => _serviceConnected; - - public DbusSNITrayIconImpl() + + public DBusTrayIconImpl() { - _connection = DBusHelper.TryGetConnection(); + _connection = DBusHelper.TryCreateNewConnection(); if (_connection is null) { @@ -46,9 +48,9 @@ namespace Avalonia.FreeDesktop } _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - InitializeSNWService(); - CreateTrayIcon(); + _serviceWatchDisposable = Watch(); } @@ -73,12 +75,10 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; } - - + private async Task Watch() => await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; - - + private void OnNameChange(ServiceOwnerChangedEventArgs obj) { if (_isDisposed) @@ -87,6 +87,7 @@ namespace Avalonia.FreeDesktop if (!_serviceConnected & obj.NewOwner != null) { _serviceConnected = true; + InitializeSNWService(); if (_isVisible) { @@ -104,8 +105,7 @@ namespace Avalonia.FreeDesktop _serviceConnected = false; } } - - public void CreateTrayIcon() + private void CreateTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) return; @@ -136,7 +136,7 @@ namespace Avalonia.FreeDesktop _isVisible = true; } - public void DestroyTrayIcon() + private void DestroyTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) return; diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 9e03dcd604..3051e90457 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -11,17 +11,17 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - _dbusSniTrayIcon = new DbusSNITrayIconImpl(); + _dBusTrayIcon = new DBusTrayIconImpl(); } - private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; + private readonly DBusTrayIconImpl _dBusTrayIcon; private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; private bool _isDisposed; public void Dispose() { - _dbusSniTrayIcon?.Dispose(); + _dBusTrayIcon?.Dispose(); _xEmbedTrayIcon?.Dispose(); _isDisposed = true; } @@ -30,12 +30,12 @@ namespace Avalonia.X11 { if (_isDisposed) return; - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { if (!(icon is X11IconData x11icon)) return; - _dbusSniTrayIcon.SetIcon(x11icon.Data); + _dBusTrayIcon.SetIcon(x11icon.Data); } else { @@ -47,9 +47,9 @@ namespace Avalonia.X11 { if (_isDisposed) return; - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - _dbusSniTrayIcon.SetToolTipText(text); + _dBusTrayIcon.SetToolTipText(text); } else { @@ -61,9 +61,9 @@ namespace Avalonia.X11 { if (_isDisposed) return; - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - _dbusSniTrayIcon.SetIsVisible(visible); + _dBusTrayIcon.SetIsVisible(visible); } else { @@ -75,9 +75,9 @@ namespace Avalonia.X11 { get { - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - return _dbusSniTrayIcon.MenuExporter; + return _dBusTrayIcon.MenuExporter; } else { @@ -90,9 +90,9 @@ namespace Avalonia.X11 { get { - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - return _dbusSniTrayIcon.OnClicked; + return _dBusTrayIcon.OnClicked; } else { @@ -101,9 +101,9 @@ namespace Avalonia.X11 } set { - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - _dbusSniTrayIcon.OnClicked = value; + _dBusTrayIcon.OnClicked = value; } else { From be94e300807ea1849f730d8e90c7c52c6aa7dcf4 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:15:09 +0800 Subject: [PATCH 04/26] simplify logic --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 2d30d2ee21..f1146ebdd9 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -89,19 +89,15 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; InitializeSNWService(); + DestroyTrayIcon(); + if (_isVisible) { - DestroyTrayIcon(); CreateTrayIcon(); } - else - { - DestroyTrayIcon(); - } } else if (_serviceConnected & obj.NewOwner is null) { - s_trayIconInstanceId = 0; _serviceConnected = false; } } @@ -110,14 +106,12 @@ namespace Avalonia.FreeDesktop if (_connection is null || !_serviceConnected || _isDisposed) return; - var pid = Process.GetCurrentProcess().Id; var tid = s_trayIconInstanceId++; _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - - + try { _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); @@ -360,11 +354,11 @@ namespace Avalonia.FreeDesktop 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. + [Dictionary] internal class StatusNotifierItemProperties { public string? Category; From 1a9558a28a7946a496cdf2d089f7a8cc91c29d8b Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:15:37 +0800 Subject: [PATCH 05/26] fix naming --- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index c2247565be..3355182a60 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -11,17 +11,17 @@ namespace Avalonia.X11 { } - private bool IsCalled; + private bool _isCalled; private void NotImplemented() { - if(IsCalled) return; + if(_isCalled) return; Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); - IsCalled = true; + _isCalled = true; } public void Dispose() From 3f348288b35fdda7cf150db10150a06357dad248 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:43:18 +0800 Subject: [PATCH 06/26] add signed build InternalsVisibleTo --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index f1146ebdd9..4252b9f26a 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -10,7 +10,13 @@ using Avalonia.Logging; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] + +#if SIGNED_BUILD +[assembly: InternalsVisibleTo("Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +#else [assembly: InternalsVisibleTo("Avalonia.X11")] +#endif + namespace Avalonia.FreeDesktop { internal class DBusTrayIconImpl From 6f16f8b66ffdcfb50cbe94905da208a7c35de018 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:50:18 +0800 Subject: [PATCH 07/26] some fixes --- src/Avalonia.FreeDesktop/DBusHelper.cs | 2 +- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 2 +- src/Avalonia.X11/X11TrayIconImpl.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index b5bfb8b116..c14539d7bf 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop /// This class uses synchronous execution at DBus connection establishment stage /// then switches to using AvaloniaSynchronizationContext /// - class DBusSyncContext : SynchronizationContext + private class DBusSyncContext : SynchronizationContext { private SynchronizationContext _ctx; private object _lock = new object(); diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 4252b9f26a..32033cb0a6 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -138,7 +138,7 @@ namespace Avalonia.FreeDesktop private void DestroyTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) return; _connection.UnregisterObject(_statusNotifierItemDbusObj); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 3051e90457..93bf71b409 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -15,7 +15,6 @@ namespace Avalonia.X11 } private readonly DBusTrayIconImpl _dBusTrayIcon; - private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; private bool _isDisposed; From ef231e68361f60ea8941e55f5300fb71b74327da Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 16:35:09 +0800 Subject: [PATCH 08/26] fix --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 32033cb0a6..9534d3a32f 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.FreeDesktop private bool _serviceConnected; private bool _isVisible = true; - public bool IsActive => _serviceConnected; + public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } @@ -53,11 +53,15 @@ namespace Avalonia.FreeDesktop return; } + IsActive = true; + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); _serviceWatchDisposable = Watch(); + + } private void InitializeSNWService() @@ -148,6 +152,7 @@ namespace Avalonia.FreeDesktop public void Dispose() { + IsActive = false; _isDisposed = true; DestroyTrayIcon(); _connection?.Dispose(); From 5871218737677086c19e51c38ee5eec5ac800cc6 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 16:48:18 +0800 Subject: [PATCH 09/26] fix some more --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 9534d3a32f..1ea5d720ce 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -108,6 +108,7 @@ namespace Avalonia.FreeDesktop } else if (_serviceConnected & obj.NewOwner is null) { + DestroyTrayIcon(); _serviceConnected = false; } } @@ -137,7 +138,6 @@ namespace Avalonia.FreeDesktop _statusNotifierItemDbusObj.SetIcon(_icon); _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - _isVisible = true; } private void DestroyTrayIcon() @@ -147,7 +147,6 @@ namespace Avalonia.FreeDesktop _connection.UnregisterObject(_statusNotifierItemDbusObj); _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isVisible = false; } public void Dispose() @@ -197,6 +196,8 @@ namespace Avalonia.FreeDesktop { DestroyTrayIcon(); } + + _isVisible = visible; } public void SetToolTipText(string? text) From c1b68b1836d117f0b97530cd7c72bb22404a22ed Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:49:16 +0800 Subject: [PATCH 10/26] refactor to remove redundant X11TrayIconImpl.cs --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 17 ++- src/Avalonia.X11/X11Platform.cs | 15 ++- src/Avalonia.X11/X11TrayIconImpl.cs | 114 ------------------- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 11 +- 4 files changed, 32 insertions(+), 125 deletions(-) delete mode 100644 src/Avalonia.X11/X11TrayIconImpl.cs diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 1ea5d720ce..9095773d90 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; +using Avalonia.Platform; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] @@ -19,7 +20,7 @@ using Tmds.DBus; namespace Avalonia.FreeDesktop { - internal class DBusTrayIconImpl + internal class DBusTrayIconImpl : ITrayIconImpl { private static int s_trayIconInstanceId; @@ -40,7 +41,8 @@ namespace Avalonia.FreeDesktop public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - + public Func IconConverterDelegate { get; set; } + public DBusTrayIconImpl() { _connection = DBusHelper.TryCreateNewConnection(); @@ -158,10 +160,15 @@ namespace Avalonia.FreeDesktop _serviceWatchDisposable?.Dispose(); } - public void SetIcon(UIntPtr[] x11iconData) + public void SetIcon(IWindowIconImpl icon) { if (_isDisposed) return; + + var x11iconData = IconConverterDelegate(icon); + + if(x11iconData.Length == 0) return; + var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -171,7 +178,7 @@ namespace Avalonia.FreeDesktop for (var i = 0; i < pixLength; i++) { - var rawPixel = x11iconData[i + 2].ToUInt32(); + var rawPixel = x11iconData[i + 2]; pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); @@ -360,7 +367,7 @@ namespace Avalonia.FreeDesktop Task WatchNewOverlayIconAsync(Action handler, Action onError); Task WatchNewToolTipAsync(Action handler, Action onError); Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); + Task GetAsync(string prop); Task GetAllAsync(); Task SetAsync(string prop, object val); Task WatchPropertiesAsync(Action handler); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index d3aeefd088..452a72bc83 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using Avalonia.Controls; @@ -106,7 +107,19 @@ namespace Avalonia.X11 public ITrayIconImpl CreateTrayIcon () { - return new X11TrayIconImpl(); + var dbusTrayIcon = new DBusTrayIconImpl(); + + if (!dbusTrayIcon.IsActive) return new XEmbedTrayIconImpl(); + + dbusTrayIcon.IconConverterDelegate = (icon) => + { + if (!(icon is X11IconData x11icon)) + return Array.Empty(); + + return x11icon.Data.Select(x=>x.ToUInt32()).ToArray(); + }; + + return dbusTrayIcon; } public IWindowImpl CreateWindow() diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs deleted file mode 100644 index 93bf71b409..0000000000 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop; -using Avalonia.Logging; -using Avalonia.Platform; - -namespace Avalonia.X11 -{ - internal class X11TrayIconImpl : ITrayIconImpl - { - public X11TrayIconImpl() - { - _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - _dBusTrayIcon = new DBusTrayIconImpl(); - } - - private readonly DBusTrayIconImpl _dBusTrayIcon; - private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; - private bool _isDisposed; - - public void Dispose() - { - _dBusTrayIcon?.Dispose(); - _xEmbedTrayIcon?.Dispose(); - _isDisposed = true; - } - - public void SetIcon(IWindowIconImpl? icon) - { - if (_isDisposed) return; - - if (_dBusTrayIcon?.IsActive ?? false) - { - if (!(icon is X11IconData x11icon)) - return; - - _dBusTrayIcon.SetIcon(x11icon.Data); - } - else - { - _xEmbedTrayIcon.SetIcon(icon); - } - } - - public void SetToolTipText(string? text) - { - if (_isDisposed) return; - - if (_dBusTrayIcon?.IsActive ?? false) - { - _dBusTrayIcon.SetToolTipText(text); - } - else - { - _xEmbedTrayIcon.SetToolTipText(text); - } - } - - public void SetIsVisible(bool visible) - { - if (_isDisposed) return; - - if (_dBusTrayIcon?.IsActive ?? false) - { - _dBusTrayIcon.SetIsVisible(visible); - } - else - { - _xEmbedTrayIcon.SetIsVisible(visible); - } - } - - public INativeMenuExporter? MenuExporter - { - get - { - if (_dBusTrayIcon?.IsActive ?? false) - { - return _dBusTrayIcon.MenuExporter; - } - else - { - return _xEmbedTrayIcon.MenuExporter; - } - } - } - - public Action? OnClicked - { - get - { - if (_dBusTrayIcon?.IsActive ?? false) - { - return _dBusTrayIcon.OnClicked; - } - else - { - return _xEmbedTrayIcon.OnClicked; - } - } - set - { - if (_dBusTrayIcon?.IsActive ?? false) - { - _dBusTrayIcon.OnClicked = value; - } - else - { - _xEmbedTrayIcon.OnClicked = value; - } - } - } - } -} diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index 3355182a60..8b9e882bba 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -5,10 +5,11 @@ using Avalonia.Platform; namespace Avalonia.X11 { - internal class XEmbedTrayIconImpl + internal class XEmbedTrayIconImpl : ITrayIconImpl { public XEmbedTrayIconImpl() { + MenuExporter = null; } private bool _isCalled; @@ -29,12 +30,12 @@ namespace Avalonia.X11 NotImplemented(); } - public void SetIcon(IWindowIconImpl? icon) + public void SetIcon(IWindowIconImpl icon) { NotImplemented(); } - public void SetToolTipText(string? text) + public void SetToolTipText(string text) { NotImplemented(); } @@ -44,7 +45,7 @@ namespace Avalonia.X11 NotImplemented(); } - public INativeMenuExporter? MenuExporter { get; } - public Action? OnClicked { get; set; } + public INativeMenuExporter MenuExporter { get; } + public Action OnClicked { get; set; } } } From 409f74b13424c048150d1a46a7da75e08d74a01f Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:52:24 +0800 Subject: [PATCH 11/26] refactor if statement to switch as suggested --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 9095773d90..dac2995e46 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -194,14 +194,15 @@ namespace Avalonia.FreeDesktop if (_isDisposed) return; - if (visible && !_isVisible) + switch (visible) { - DestroyTrayIcon(); - CreateTrayIcon(); - } - else if (!visible && _isVisible) - { - DestroyTrayIcon(); + case true when !_isVisible: + DestroyTrayIcon(); + CreateTrayIcon(); + break; + case false when _isVisible: + DestroyTrayIcon(); + break; } _isVisible = visible; From 2d565010154f921d929d6e25308fefc7c65a1f3c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:54:44 +0800 Subject: [PATCH 12/26] suppress warning and make a log event that probably will never happen in real life --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index dac2995e46..aa18e613cb 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -62,8 +62,14 @@ namespace Avalonia.FreeDesktop MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); _serviceWatchDisposable = Watch(); - - + + IconConverterDelegate = impl => + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "The icon converter delegate for the DBus Tray Icons was not set properly."); + + return new uint[] { }; + }; } private void InitializeSNWService() From 66bb61c7a236e5acf7d3dac545e001473688d5fb Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:55:21 +0800 Subject: [PATCH 13/26] suppress warning --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index aa18e613cb..28f1b62f8a 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.FreeDesktop private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private readonly IDisposable _serviceWatchDisposable; + private readonly IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private IStatusNotifierWatcher? _statusNotifierWatcher; @@ -41,7 +41,7 @@ namespace Avalonia.FreeDesktop public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public Func IconConverterDelegate { get; set; } + public Func? IconConverterDelegate { get; set; } public DBusTrayIconImpl() { @@ -62,14 +62,6 @@ namespace Avalonia.FreeDesktop MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); _serviceWatchDisposable = Watch(); - - IconConverterDelegate = impl => - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) - ?.Log(this, "The icon converter delegate for the DBus Tray Icons was not set properly."); - - return new uint[] { }; - }; } private void InitializeSNWService() From dff7efbec8c827694b081b8a486f345a74b4f5cb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 26 Oct 2021 16:38:24 +0100 Subject: [PATCH 14/26] add command to trayicon. --- src/Avalonia.Controls/NativeMenuItem.cs | 3 +- src/Avalonia.Controls/TrayIcon.cs | 49 ++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2eaf24d2f2..2ceaeb6dba 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -16,6 +16,7 @@ namespace Avalonia.Controls private bool _isChecked = false; private NativeMenuItemToggleType _toggleType; private IBitmap _icon; + private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; private NativeMenu _menu; @@ -47,8 +48,6 @@ namespace Avalonia.Controls } } - private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber; - public NativeMenuItem() { diff --git a/src/Avalonia.Controls/TrayIcon.cs b/src/Avalonia.Controls/TrayIcon.cs index 6bfddfa877..59edb6278a 100644 --- a/src/Avalonia.Controls/TrayIcon.cs +++ b/src/Avalonia.Controls/TrayIcon.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Windows.Input; using Avalonia.Collections; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Platform; +using Avalonia.Utilities; #nullable enable @@ -13,10 +15,13 @@ namespace Avalonia.Controls public sealed class TrayIcons : AvaloniaList { } + + public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable { private readonly ITrayIconImpl? _impl; + private ICommand? _command; private TrayIcon(ITrayIconImpl? impl) { @@ -26,7 +31,15 @@ namespace Avalonia.Controls _impl.SetIsVisible(IsVisible); - _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty); + _impl.OnClicked = () => + { + Clicked?.Invoke(this, EventArgs.Empty); + + if (Command?.CanExecute(CommandParameter) == true) + { + Command.Execute(CommandParameter); + } + }; } } @@ -64,6 +77,21 @@ namespace Avalonia.Controls /// on OSX this event is not raised. /// public event EventHandler? Clicked; + + /// + /// Defines the property. + /// + public static readonly DirectProperty CommandProperty = + Button.CommandProperty.AddOwner( + trayIcon => trayIcon.Command, + (trayIcon, command) => trayIcon.Command = command, + enableDataValidation: true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty CommandParameterProperty = + Button.CommandParameterProperty.AddOwner(); /// /// Defines the attached property. @@ -98,6 +126,25 @@ namespace Avalonia.Controls public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); + + /// + /// Gets or sets the property of a TrayIcon. + /// + public ICommand? Command + { + get => _command; + set => SetAndRaise(CommandProperty, ref _command, value); + } + + /// + /// Gets or sets the parameter to pass to the property of a + /// . + /// + public object CommandParameter + { + get { return GetValue(CommandParameterProperty); } + set { SetValue(CommandParameterProperty, value); } + } /// /// Gets or sets the Menu of the TrayIcon. From 7603e57ae9c90e45e773e85a7a433d9e1e48d5e4 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Tue, 26 Oct 2021 23:48:23 +0800 Subject: [PATCH 15/26] fix review comments --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 28f1b62f8a..29b1d7fa72 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -49,7 +49,7 @@ namespace Avalonia.FreeDesktop if (_connection is null) { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + Logger.TryGet(LogEventLevel.Error, "DBUS") ?.Log(this, "Unable to get a dbus connection for system tray icons."); return; @@ -76,9 +76,9 @@ namespace Avalonia.FreeDesktop } catch { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + Logger.TryGet(LogEventLevel.Error, "DBUS") ?.Log(this, - "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); + "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it."); return; } @@ -131,6 +131,9 @@ namespace Avalonia.FreeDesktop } catch (Exception e) { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, $"Error creating a DBus tray icon: {e}."); + _serviceConnected = false; } From d819bd09798165c269f07f5403391c596eb71371 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:04:34 +0800 Subject: [PATCH 16/26] some more fixes --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 29b1d7fa72..4f561793ba 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -86,8 +86,8 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; } - private async Task Watch() => - await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + private Task Watch() => + _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; private void OnNameChange(ServiceOwnerChangedEventArgs obj) { @@ -306,9 +306,9 @@ namespace Avalonia.FreeDesktop return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public async Task GetAsync(string prop) + public Task GetAsync(string prop) { - return prop switch + return Task.FromResult(prop switch { nameof(_backingProperties.Category) => _backingProperties.Category, nameof(_backingProperties.Id) => _backingProperties.Id, @@ -318,7 +318,7 @@ namespace Avalonia.FreeDesktop nameof(_backingProperties.Title) => _backingProperties.Title, nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, _ => null - }; + }); } public Task GetAllAsync() => Task.FromResult(_backingProperties); From 6fc61952e738f8637fcef6cb8d34b22172f07c71 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:04:57 +0800 Subject: [PATCH 17/26] more fixes --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 4f561793ba..38e8c8447c 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -61,7 +61,7 @@ namespace Avalonia.FreeDesktop MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - _serviceWatchDisposable = Watch(); + _serviceWatchDisposable = WatchAsync(); } private void InitializeSNWService() @@ -86,7 +86,7 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; } - private Task Watch() => + private Task WatchAsync() => _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; private void OnNameChange(ServiceOwnerChangedEventArgs obj) From 8590607358c39e4659a06d28da987ca2b6e222ef Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:09:30 +0800 Subject: [PATCH 18/26] fix warnings --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 38e8c8447c..c573df2290 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -161,9 +161,9 @@ namespace Avalonia.FreeDesktop _serviceWatchDisposable?.Dispose(); } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) + if (_isDisposed || IconConverterDelegate is null) return; var x11iconData = IconConverterDelegate(icon); From 22f2a011822c9f7dab3300265929aac628cf6283 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:10:17 +0800 Subject: [PATCH 19/26] ignore if passed with null icon --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index c573df2290..7bbed8c702 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -163,7 +163,7 @@ namespace Avalonia.FreeDesktop public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed || IconConverterDelegate is null) + if (icon is null || _isDisposed || IconConverterDelegate is null) return; var x11iconData = IconConverterDelegate(icon); From 27cfa606f2b207692a34d844e6c25f87d3777031 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:27:08 +0800 Subject: [PATCH 20/26] Use empty icon if seticon receives null --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 7bbed8c702..c7ff775d6f 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -163,9 +163,15 @@ namespace Avalonia.FreeDesktop public void SetIcon(IWindowIconImpl? icon) { - if (icon is null || _isDisposed || IconConverterDelegate is null) + if (_isDisposed || IconConverterDelegate is null) return; + if (icon is null) + { + _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); + return; + } + var x11iconData = IconConverterDelegate(icon); if(x11iconData.Length == 0) return; @@ -434,5 +440,7 @@ namespace Avalonia.FreeDesktop Height = height; Data = data; } + + public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 }); } } From fc4c0b09ca93b5857e329f1dcfc33fdc52d98def Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:34:40 +0800 Subject: [PATCH 21/26] make the icon converter delegate static to avoid unnecessary allocs --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 2 +- src/Avalonia.X11/X11Platform.cs | 22 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index c7ff775d6f..58e8a8efc8 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.FreeDesktop public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public Func? IconConverterDelegate { get; set; } + public Func IconConverterDelegate { get; set; } public DBusTrayIconImpl() { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 452a72bc83..a2bd57404b 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -105,23 +105,25 @@ namespace Avalonia.X11 public IntPtr DeferredDisplay { get; set; } public IntPtr Display { get; set; } - public ITrayIconImpl CreateTrayIcon () + private static uint[] X11IconConverter(IWindowIconImpl? icon) + { + if (!(icon is X11IconData x11icon)) + return Array.Empty(); + + return x11icon.Data.Select(x => x.ToUInt32()).ToArray(); + } + + public ITrayIconImpl CreateTrayIcon() { var dbusTrayIcon = new DBusTrayIconImpl(); if (!dbusTrayIcon.IsActive) return new XEmbedTrayIconImpl(); - - dbusTrayIcon.IconConverterDelegate = (icon) => - { - if (!(icon is X11IconData x11icon)) - return Array.Empty(); - return x11icon.Data.Select(x=>x.ToUInt32()).ToArray(); - }; - + dbusTrayIcon.IconConverterDelegate = X11IconConverter; + return dbusTrayIcon; } - + public IWindowImpl CreateWindow() { return new X11Window(this, null); From e0fff15af3b696842a96f9724a778c7cbb4f0938 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 27 Oct 2021 00:45:02 +0800 Subject: [PATCH 22/26] handle WatchAsync better --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 55 +++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 58e8a8efc8..8a16cf4c34 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -13,7 +13,9 @@ using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] #if SIGNED_BUILD -[assembly: InternalsVisibleTo("Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: + InternalsVisibleTo( + "Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] #else [assembly: InternalsVisibleTo("Avalonia.X11")] #endif @@ -23,10 +25,10 @@ namespace Avalonia.FreeDesktop internal class DBusTrayIconImpl : ITrayIconImpl { private static int s_trayIconInstanceId; - + private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private readonly IDisposable? _serviceWatchDisposable; + private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private IStatusNotifierWatcher? _statusNotifierWatcher; @@ -37,11 +39,11 @@ namespace Avalonia.FreeDesktop private bool _isDisposed; private bool _serviceConnected; private bool _isVisible = true; - + public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public Func IconConverterDelegate { get; set; } + public Func IconConverterDelegate { get; set; } public DBusTrayIconImpl() { @@ -56,12 +58,12 @@ namespace Avalonia.FreeDesktop } IsActive = true; - + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - - _serviceWatchDisposable = WatchAsync(); + + WatchAsync(); } private void InitializeSNWService() @@ -85,10 +87,22 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; } - - private Task WatchAsync() => - _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; - + + private async void WatchAsync() + { + try + { + _serviceWatchDisposable = + await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, + $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}"); + } + } + private void OnNameChange(ServiceOwnerChangedEventArgs obj) { if (_isDisposed) @@ -112,6 +126,7 @@ namespace Avalonia.FreeDesktop _serviceConnected = false; } } + private void CreateTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) @@ -122,7 +137,7 @@ namespace Avalonia.FreeDesktop _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - + try { _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); @@ -133,7 +148,7 @@ namespace Avalonia.FreeDesktop { Logger.TryGet(LogEventLevel.Error, "DBUS") ?.Log(this, $"Error creating a DBus tray icon: {e}."); - + _serviceConnected = false; } @@ -170,12 +185,12 @@ namespace Avalonia.FreeDesktop { _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); return; - } - + } + var x11iconData = IconConverterDelegate(icon); - - if(x11iconData.Length == 0) return; - + + if (x11iconData.Length == 0) return; + var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; From cede7dfab8b8c1cf0d0c2c67d1aab54655e4e058 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 26 Oct 2021 17:49:25 +0100 Subject: [PATCH 23/26] make delegate nullable. --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 8a16cf4c34..c681415505 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -43,7 +43,7 @@ namespace Avalonia.FreeDesktop public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public Func IconConverterDelegate { get; set; } + public Func? IconConverterDelegate { get; set; } public DBusTrayIconImpl() { From 91802c1f4fd096802e262eb5b12cd9a24f826887 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 26 Oct 2021 19:49:26 +0300 Subject: [PATCH 24/26] minor nit --- src/Avalonia.X11/X11Platform.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a2bd57404b..8ff3b4f5e0 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -105,7 +105,7 @@ namespace Avalonia.X11 public IntPtr DeferredDisplay { get; set; } public IntPtr Display { get; set; } - private static uint[] X11IconConverter(IWindowIconImpl? icon) + private static uint[] X11IconConverter(IWindowIconImpl icon) { if (!(icon is X11IconData x11icon)) return Array.Empty(); From 740264d2daf54dcec5c53818b3c5dc76ad0831c3 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 26 Oct 2021 19:50:28 +0300 Subject: [PATCH 25/26] minor nit --- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index 8b9e882bba..c046e49f93 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -7,10 +7,6 @@ namespace Avalonia.X11 { internal class XEmbedTrayIconImpl : ITrayIconImpl { - public XEmbedTrayIconImpl() - { - MenuExporter = null; - } private bool _isCalled; From 90b251bf3a520b0c76ca68829fa21ba1e3ca5668 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Tue, 26 Oct 2021 20:03:03 +0300 Subject: [PATCH 26/26] cleanup --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index c681415505..a7cc4f4cc2 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -12,13 +12,9 @@ using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -#if SIGNED_BUILD [assembly: InternalsVisibleTo( "Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] -#else -[assembly: InternalsVisibleTo("Avalonia.X11")] -#endif namespace Avalonia.FreeDesktop {