diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 2ad220dddd..08ed8669ef 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -39,5 +39,10 @@ namespace Avalonia.Logging /// The log event comes from Win32Platform. /// public const string Win32Platform = nameof(Win32Platform); + + /// + /// The log event comes from Win32Platform. + /// + public const string X11Platform = nameof(X11Platform); } } diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 7996a94dd0..4e23711ed4 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop public static Connection TryInitialize(string dbusAddress = null) { - if (Connection != null) - return Connection; + return Connection ?? TryGetConnection(dbusAddress); + } + + public static Connection TryGetConnection(string dbusAddress = null) + { var oldContext = SynchronizationContext.Current; try { diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index b5e35db969..932b876088 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -8,6 +8,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using Tmds.DBus; #pragma warning disable 1998 @@ -16,15 +17,23 @@ namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid) + public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid) { if (DBusHelper.Connection == null) return null; return new DBusMenuExporterImpl(DBusHelper.Connection, xid); } + + public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) + { + return new DBusMenuExporterImpl(currentConection, path); + } + + public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" + + Guid.NewGuid().ToString().Replace("-", ""); - class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable { private readonly Connection _dbus; private readonly uint _xid; @@ -37,30 +46,48 @@ namespace Avalonia.FreeDesktop private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; + private bool AppMenu = true; public DBusMenuExporterImpl(Connection dbus, IntPtr xid) { _dbus = dbus; _xid = (uint)xid.ToInt32(); - ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString().Replace("-", "")); + ObjectPath = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); Init(); } + public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + { + _dbus = dbus; + AppMenu = false; + ObjectPath = path; + SetNativeMenu(new NativeMenu()); + Init(); + } + async void Init() { try { - await _dbus.RegisterObjectAsync(this); - _registar = DBusHelper.Connection.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed) - await _registar.RegisterWindowAsync(_xid, ObjectPath); + if (AppMenu) + { + await _dbus.RegisterObjectAsync(this); + _registar = DBusHelper.Connection.CreateProxy( + "com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registar.RegisterWindowAsync(_xid, ObjectPath); + } + else + { + await _dbus.RegisterObjectAsync(this); + } } catch (Exception e) { - Console.Error.WriteLine(e); + Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) + ?.Log(this, e.Message); + // It's not really important if this code succeeds, // and it's not important to know if it succeeds // since even if we register the window it's not guaranteed that @@ -248,17 +275,24 @@ namespace Avalonia.FreeDesktop if (item.ToggleType != NativeMenuItemToggleType.None) return item.IsChecked ? 1 : 0; } - + if (name == "icon-data") { if (item.Icon != null) { - var ms = new MemoryStream(); - item.Icon.Save(ms); - return ms.ToArray(); + var loader = AvaloniaLocator.Current.GetService(); + + if (loader != null) + { + var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); + + using var ms = new MemoryStream(); + icon.Save(ms); + return ms.ToArray(); + } } } - + if (name == "children-display") return menu != null ? "submenu" : null; } diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 8909a4604b..99a850dabb 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,33 +1,374 @@ using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.FreeDesktop; using Avalonia.Platform; +using Tmds.DBus; + +[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] namespace Avalonia.X11 { - class X11TrayIconImpl : ITrayIconImpl + internal class X11TrayIconImpl : ITrayIconImpl { - public INativeMenuExporter MenuExporter => null; + + private static int s_trayIconInstanceId = 0; + private static int GetTID() => s_trayIconInstanceId++; + private ObjectPath _dbusmenuPath; + private StatusNotifierItemDbusObj _statusNotifierItemDbusObj; + private Connection con; + 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() + { + con = DBusHelper.TryGetConnection(); + _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con); + CreateTrayIcon(); + _ctorFinished = true; + } + + public async void CreateTrayIcon() + { + _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher", + "/StatusNotifierWatcher"); + + var pid = Process.GetCurrentProcess().Id; + var tid = GetTID(); + + _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath); + + await con.RegisterObjectAsync(_statusNotifierItemDbusObj); + + await con.RegisterServiceAsync(_sysTrayServiceName); + + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + + _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); + _statusNotifierItemDbusObj.SetIcon(_icon); + + _statusNotifierItemDbusObj.ActivationDelegate = () => + { + OnClicked?.Invoke(); + }; + + _isActive = true; + } + + public async void DestroyTrayIcon() + { + con.UnregisterObject(_statusNotifierItemDbusObj); + await con.UnregisterServiceAsync(_sysTrayServiceName); + _isActive = false; + } public void Dispose() { - + _isDisposed = true; + DestroyTrayIcon(); + con.Dispose(); } public void SetIcon(IWindowIconImpl icon) { - - } + if (con == null || _isDisposed) return; + if (!(icon is X11IconData x11icon)) return; + + var w = (int)x11icon.Data[0]; + var h = (int)x11icon.Data[1]; + + var rx = x11icon.Data.AsSpan(2); + var pixLength = w * h; + + var pixByteArrayCounter = 0; + var pixByteArray = new byte[w * h * 4]; + for (var i = 0; i < pixLength; i++) + { + var u = rx[i].ToUInt32(); + pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF000000) >> 24); + pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF0000) >> 16); + pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF00) >> 8); + pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF); + } + + _icon = new DbusPixmap(w, h, pixByteArray); + _statusNotifierItemDbusObj.SetIcon(_icon); + } + public void SetIsVisible(bool visible) { - + if (con == null || _isDisposed || !_ctorFinished) return; + + if (visible & !_isActive) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else if (!visible & _isActive) + { + DestroyTrayIcon(); + } } public void SetToolTipText(string text) { - + if (con == null || _isDisposed) 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; + private event Action OnPropertyChange; + public event Action OnTitleChanged; + public event Action OnIconChanged; + public event Action OnAttentionIconChanged; + public event Action OnOverlayIconChanged; + public event Action OnTooltipChanged; + public Action SetNativeMenuExporter { get; set; } + 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 async Task ContextMenuAsync(int X, int Y) + { + } + + public async Task ActivateAsync(int X, int Y) + { + ActivationDelegate?.Invoke(); + } + + public async Task SecondaryActivateAsync(int X, int Y) + { + } + + public async Task ScrollAsync(int Delta, string Orientation) + { + } + + public void InvalidateAll() + { + OnTitleChanged?.Invoke(); + OnIconChanged?.Invoke(); + OnOverlayIconChanged?.Invoke(); + OnAttentionIconChanged?.Invoke(); + OnTooltipChanged?.Invoke(); + } + + public async Task WatchNewTitleAsync(Action handler, Action onError = null) + { + OnTitleChanged += handler; + return Disposable.Create(() => OnTitleChanged -= handler); + } + + public async Task WatchNewIconAsync(Action handler, Action onError = null) + { + OnIconChanged += handler; + return Disposable.Create(() => OnIconChanged -= handler); + } + + public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null) + { + OnAttentionIconChanged += handler; + return Disposable.Create(() => OnAttentionIconChanged -= handler); + } + + public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null) + { + OnOverlayIconChanged += handler; + return Disposable.Create(() => OnOverlayIconChanged -= handler); + } + + public async Task WatchNewToolTipAsync(Action handler, Action onError = null) + { + OnTooltipChanged += handler; + return Disposable.Create(() => OnTooltipChanged -= handler); + } + + public async Task WatchNewStatusAsync(Action handler, Action onError = null) + { + NewStatusAsync += handler; + return Disposable.Create(() => NewStatusAsync -= handler); + } + + public async Task GetAsync(string prop) + { + if (prop.Contains("Menu")) + { + return _backingProperties.Menu; + } + + return default; + } + + public async Task GetAllAsync() + { + return _backingProperties; + } + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public async Task WatchPropertiesAsync(Action handler) + { + OnPropertyChange += handler; + return Disposable.Create(() => OnPropertyChange -= handler); + } + + public void SetIcon(DbusPixmap dbusPixmap) + { + _backingProperties.IconPixmap = new[] { dbusPixmap }; + InvalidateAll(); + } + + public void SetTitleAndTooltip(string text) + { + _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 = null); + Task WatchNewIconAsync(Action handler, Action onError = null); + Task WatchNewAttentionIconAsync(Action handler, Action onError = null); + Task WatchNewOverlayIconAsync(Action handler, Action onError = null); + Task WatchNewToolTipAsync(Action handler, Action onError = null); + Task WatchNewStatusAsync(Action handler, Action onError = null); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action handler); + } + + [Dictionary] + internal class StatusNotifierItemProperties + { + public string Category; + + public string Id; + + public string Title; + + public string Status; + + public int WindowId; + + public string IconThemePath; + + public ObjectPath Menu; + + public bool ItemIsMenu; + + public string IconName; + + public DbusPixmap[] IconPixmap; + + public string OverlayIconName; + + public DbusPixmap[] OverlayIconPixmap; + + public string AttentionIconName; + + public DbusPixmap[] AttentionIconPixmap; + + public string AttentionMovieName; + + 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, new byte[] { }), new DbusPixmap(0, 0, new 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; } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index f0d2d5ca8a..e39be6fc04 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -190,7 +190,7 @@ namespace Avalonia.X11 if(_popup) PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize)); if (platform.Options.UseDBusMenu) - NativeMenuExporter = DBusMenuExporter.TryCreate(_handle); + NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle); NativeControlHost = new X11NativeControlHost(_platform, this); DispatcherTimer.Run(() => {