From d3e773ab0b5b5f42f9d33a6ce45cb0549704ca07 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:32:05 +0800 Subject: [PATCH] Update C# code to use named DBus struct types Replace auto-generated DbusStruct_* names with meaningful type names from the av:TypeDefinition annotations added to the DBus XML files: - DbusStruct_Riaesvavz -> MenuLayout - DbusStruct_Riaesvz -> MenuItemProperties - DbusStruct_Risvuz -> MenuEvent - DbusStruct_Riiayz -> IconPixmap - DbusStruct_Rsariiayzssz -> ToolTip - DbusStruct_Rssz -> InputContextArg - DbusStruct_Rsiz -> FormattedPreeditSegment Also update property accessors (.Item1/.Item2 -> named properties) in DBusMenuExporter.cs and FcitxX11TextInputMethod.cs. --- .../DBusIme/Fcitx/FcitxICWrapper.cs | 26 +-- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 53 +++--- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 152 +++++++++++------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 137 +++++++++++----- 4 files changed, 226 insertions(+), 142 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 295a36b060..b70ec97ccc 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.DBus; +using Avalonia.FreeDesktop.DBusXml; using Avalonia.Reactive; -using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { @@ -38,23 +40,23 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public ValueTask WatchCommitStringAsync(Action handler) => + public Task WatchCommitStringAsync(Action handler) => _old?.WatchCommitStringAsync(handler) ?? _modern?.WatchCommitStringAsync(handler) - ?? new ValueTask(Disposable.Empty); + ?? Task.FromResult(Disposable.Empty); - public ValueTask WatchForwardKeyAsync(Action handler) => + public Task WatchForwardKeyAsync(Action handler) => _old?.WatchForwardKeyAsync(handler) - ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0))) - ?? new ValueTask(Disposable.Empty); + ?? _modern?.WatchForwardKeyAsync((keyval, state, type) => handler.Invoke(keyval, state, type ? 1 : 0)) + ?? Task.FromResult(Disposable.Empty); - public ValueTask WatchUpdateFormattedPreeditAsync( - Action handler) => - _old?.WatchUpdateFormattedPreeditAsync(handler!) - ?? _modern?.WatchUpdateFormattedPreeditAsync(handler!) - ?? new ValueTask(Disposable.Empty); + public Task WatchUpdateFormattedPreeditAsync( + Action, int> handler) => + _old?.WatchUpdateFormattedPreeditAsync(handler) + ?? _modern?.WatchUpdateFormattedPreeditAsync(handler) + ?? Task.FromResult(Disposable.Empty); public Task SetCapacityAsync(uint flags) => - _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; + _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync((ulong)flags) ?? Task.CompletedTask; } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index f50c33a5d8..462c691fcc 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,14 +1,15 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.DBus; +using Avalonia.FreeDesktop.DBusXml; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; -using Tmds.DBus.Protocol; -using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { @@ -19,23 +20,23 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx private FcitxCapabilityFlags _optionFlags; private FcitxCapabilityFlags _capabilityFlags; - public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } + public FcitxX11TextInputMethod(DBusConnection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } protected override async Task Connect(string name) { if (name == "org.fcitx.Fcitx") { - var method = new OrgFcitxFcitxInputMethodProxy(Connection, name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethodProxy(Connection, name, new DBusObjectPath("/inputmethod")); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = new OrgFcitxFcitxInputContextProxy(Connection, name, $"/inputcontext_{resp.Icid}"); + var proxy = new OrgFcitxFcitxInputContextProxy(Connection, name, new DBusObjectPath($"/inputcontext_{resp.Icid}")); _context = new FcitxICWrapper(proxy); } else { - var method = new OrgFcitxFcitxInputMethod1Proxy(Connection, name, "/inputmethod"); - var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); + var method = new OrgFcitxFcitxInputMethod1Proxy(Connection, name, new DBusObjectPath("/inputmethod")); + var resp = await method.CreateInputContextAsync(new List { new InputContextArg("appName", GetAppName()) }); var proxy = new OrgFcitxFcitxInputContext1Proxy(Connection, name, resp.Item1); _context = new FcitxICWrapper(proxy); } @@ -46,23 +47,23 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - private void OnPreedit(Exception? arg1, ((string?, int)[]? str, int cursorpos) args) + private void OnPreedit(List str, int cursorpos) { int? cursor = null; string? preeditString = null; - if (args.str != null && args.str.Length > 0) + if (str.Count > 0) { - preeditString = string.Join("", args.str.Select(x => x.Item1)); + preeditString = string.Join("", str.Select(x => x.Text)); - if (preeditString.Length > 0 && args.cursorpos >= 0) + if (preeditString.Length > 0 && cursorpos >= 0) { // cursorpos is a byte offset in UTF8 sequence that got sent through dbus - // Tmds.DBus has already converted it to UTF16, so we need to convert it back + // The DBus library has already converted it to UTF16, so we need to convert it back // and figure out the byte offset var utf8String = Encoding.UTF8.GetBytes(preeditString); - if (utf8String.Length >= args.cursorpos) + if (utf8String.Length >= cursorpos) { - cursor = Encoding.UTF8.GetCharCount(utf8String, 0, args.cursorpos); + cursor = Encoding.UTF8.GetCharCount(utf8String, 0, cursorpos); } } } @@ -168,23 +169,23 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return PushFlagsIfNeeded(); }); - private void OnForward(Exception? e, (uint keyval, uint state, int type) ev) + private void OnForward(uint keyval, uint state, int type) { - var state = (FcitxKeyState)ev.state; + var modState = (FcitxKeyState)state; KeyModifiers mods = default; - if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Ctrl)) + if (modState.HasAllFlags(FcitxKeyState.FcitxKeyState_Ctrl)) mods |= KeyModifiers.Control; - if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Alt)) + if (modState.HasAllFlags(FcitxKeyState.FcitxKeyState_Alt)) mods |= KeyModifiers.Alt; - if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Shift)) + if (modState.HasAllFlags(FcitxKeyState.FcitxKeyState_Shift)) mods |= KeyModifiers.Shift; - if (state.HasAllFlags(FcitxKeyState.FcitxKeyState_Super)) + if (modState.HasAllFlags(FcitxKeyState.FcitxKeyState_Super)) mods |= KeyModifiers.Meta; - var isPressKey = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY; + var isPressKey = type == (int)FcitxKeyEventType.FCITX_PRESS_KEY; FireForward(new X11InputMethodForwardedKey { Modifiers = mods, - KeyVal = (int)ev.keyval, + KeyVal = (int)keyval, Type = isPressKey ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, @@ -192,14 +193,8 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(Exception? e, string s) + private void OnCommitString(string s) { - if (e is not null) - { - Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, $"OnCommitString failed: {e}"); - return; - } - FireCommit(s); } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 78595d7bb0..d1966f66b4 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -6,30 +6,33 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; +using Avalonia.DBus; +using Avalonia.FreeDesktop.DBusXml; using Avalonia.Input; +using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Threading; -using Tmds.DBus.Protocol; -using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { internal class DBusMenuExporter { public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) => - DBusHelper.DefaultConnection is {} conn ? new DBusMenuExporterImpl(conn, xid) : null; + DBusHelper.DefaultConnection is { } conn ? new DBusMenuExporterImpl(conn, xid) : null; - public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) => + public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, DBusConnection currentConnection) => new DBusMenuExporterImpl(currentConnection, path); public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private sealed class DBusMenuExporterImpl : ComCanonicalDbusmenuHandler, ITopLevelNativeMenuExporter, IDisposable + private sealed class DBusMenuExporterImpl : IComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { + private const string InterfaceName = "com.canonical.dbusmenu"; + private readonly DBusConnection _connection; private readonly Dictionary _idsToItems = new(); private readonly Dictionary _itemsToIds = new(); private readonly HashSet _menus = []; - private readonly PathHandler _pathHandler; + private readonly string _path; private readonly uint _xid; private readonly bool _appMenu = true; private ComCanonicalAppMenuRegistrarProxy? _registrar; @@ -38,79 +41,99 @@ namespace Avalonia.FreeDesktop private uint _revision = 1; private bool _resetQueued; private int _nextId = 1; + private IDisposable? _registration; - public DBusMenuExporterImpl(Connection connection, IntPtr xid) + public DBusMenuExporterImpl(DBusConnection connection, IntPtr xid) { Version = 4; - Connection = connection; + _connection = connection; _xid = (uint)xid.ToInt32(); - _pathHandler = new PathHandler(GenerateDBusMenuObjPath); - _pathHandler.Add(this); + _path = GenerateDBusMenuObjPath; SetNativeMenu([]); _ = InitializeAsync(); } - public DBusMenuExporterImpl(Connection connection, string path) + public DBusMenuExporterImpl(DBusConnection connection, string path) { Version = 4; - Connection = connection; + _connection = connection; _appMenu = false; - _pathHandler = new PathHandler(path); - _pathHandler.Add(this); + _path = path; SetNativeMenu([]); _ = InitializeAsync(); } - public override Connection Connection { get; } + // IComCanonicalDbusmenu properties + public uint Version { get; } + public string TextDirection { get; } = "ltr"; + public string Status { get; } = "normal"; + public List IconThemePath { get; } = []; - protected override ValueTask<(uint Revision, (int, Dictionary, VariantValue[]) Layout)> OnGetLayoutAsync(Message message, int parentId, int recursionDepth, string[] propertyNames) + // IComCanonicalDbusmenu methods + public ValueTask<(uint Revision, MenuLayout Layout)> GetLayoutAsync(int parentId, int recursionDepth, List propertyNames) { var menu = GetMenu(parentId); - var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames?.ToArray() ?? []); if (!IsNativeMenuExported) { IsNativeMenuExported = true; OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); } - return new ValueTask<(uint, (int, Dictionary, VariantValue[]))>((_revision, layout)); + return new ValueTask<(uint, MenuLayout)>((_revision, layout)); } - protected override ValueTask<(int, Dictionary)[]> OnGetGroupPropertiesAsync(Message message, int[] ids, string[] propertyNames) - => new(ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray()); + public ValueTask> GetGroupPropertiesAsync(List ids, List propertyNames) + { + var names = propertyNames?.ToArray() ?? []; + var result = ids.Select(id => new MenuItemProperties(id, GetProperties(GetMenu(id), names))).ToList(); + return new ValueTask>(result); + } - protected override ValueTask OnGetPropertyAsync(Message message, int id, string name) => - new(GetProperty(GetMenu(id), name) ?? VariantValue.Int32(0)); + public ValueTask GetPropertyAsync(int id, string name) => + new(GetProperty(GetMenu(id), name) ?? new DBusVariant(0)); - protected override ValueTask OnEventAsync(Message message, int id, string eventId, VariantValue data, uint timestamp) + public ValueTask EventAsync(int id, string eventId, DBusVariant data, uint timestamp) { HandleEvent(id, eventId); return new ValueTask(); } - protected override ValueTask OnEventGroupAsync(Message message, (int, string, VariantValue, uint)[] events) + public ValueTask> EventGroupAsync(List events) { foreach (var e in events) - HandleEvent(e.Item1, e.Item2); - return new ValueTask([]); + HandleEvent(e.Id, e.EventId); + return new ValueTask>([]); } - protected override ValueTask OnAboutToShowAsync(Message message, int id) => new(false); + public ValueTask AboutToShowAsync(int id) => new(false); - protected override ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> OnAboutToShowGroupAsync(Message message, int[] ids) => - new(([], [])); + public ValueTask<(List UpdatesNeeded, List IdErrors)> AboutToShowGroupAsync(List ids) => + new((new List(), new List())); private async Task InitializeAsync() { - Connection.AddMethodHandler(_pathHandler); + try + { + _registration = await _connection.RegisterObjects( + (DBusObjectPath)_path, + new object[] { this }); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Failed to register dbusmenu handler: {Exception}", e); + return; + } + if (!_appMenu) return; - _registrar = new ComCanonicalAppMenuRegistrarProxy(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); + _registrar = new ComCanonicalAppMenuRegistrarProxy(_connection, "com.canonical.AppMenu.Registrar", new DBusObjectPath("/com/canonical/AppMenu/Registrar")); try { if (!_disposed) - await _registrar.RegisterWindowAsync(_xid, _pathHandler.Path); + await _registrar.RegisterWindowAsync(_xid, (DBusObjectPath)_path); } catch { @@ -129,8 +152,8 @@ namespace Avalonia.FreeDesktop _disposed = true; // Fire and forget _ = _registrar?.UnregisterWindowAsync(_xid); - _pathHandler.Remove(this); - Connection.RemoveMethodHandler(_pathHandler.Path); + _registration?.Dispose(); + _registration = null; } public bool IsNativeMenuExported { get; private set; } @@ -152,7 +175,7 @@ namespace Avalonia.FreeDesktop /* This is basic initial implementation, so we don't actually track anything and just reset the whole layout on *ANY* change - + This is not how it should work and will prevent us from implementing various features, but that's the fastest way to get things working, so... */ @@ -170,6 +193,16 @@ namespace Avalonia.FreeDesktop EmitLayoutUpdated(_revision, 0); } + private void EmitLayoutUpdated(uint revision, int parent) + { + var message = DBusMessage.CreateSignal( + (DBusObjectPath)_path, + InterfaceName, + "LayoutUpdated", + revision, parent); + _ = _connection.SendMessageAsync(message); + } + private void QueueReset() { if(_resetQueued) @@ -211,32 +244,32 @@ namespace Avalonia.FreeDesktop private static readonly string[] s_allProperties = ["type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"]; - private static VariantValue? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + private static DBusVariant? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; if (it is NativeMenuItemSeparator) { if (name == "type") - return VariantValue.String("separator"); + return new DBusVariant("separator"); } else if (it is NativeMenuItem item) { if (name == "type") return null; if (name == "label") - return VariantValue.String(item.Header ?? ""); + return new DBusVariant(item.Header ?? ""); if (name == "enabled") { if (item.Menu is not null && item.Menu.Items.Count == 0) - return VariantValue.Bool(false); + return new DBusVariant(false); if (!item.IsEnabled) - return VariantValue.Bool(false); + return new DBusVariant(false); return null; } if (name == "visible") - return VariantValue.Bool(item.IsVisible); + return new DBusVariant(item.IsVisible); if (name == "shortcut") { @@ -244,7 +277,7 @@ namespace Avalonia.FreeDesktop return null; if (item.Gesture.KeyModifiers == 0) return null; - var lst = new Array(); + var lst = new List(); var mod = item.Gesture; if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) lst.Add("Control"); @@ -255,19 +288,19 @@ namespace Avalonia.FreeDesktop if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) lst.Add("Super"); lst.Add(item.Gesture.Key.ToString()); - return new Array> { lst }.AsVariantValue(); + return new DBusVariant(new List> { lst }); } if (name == "toggle-type") { if (item.ToggleType == MenuItemToggleType.CheckBox) - return VariantValue.String("checkmark"); + return new DBusVariant("checkmark"); if (item.ToggleType == MenuItemToggleType.Radio) - return VariantValue.String("radio"); + return new DBusVariant("radio"); } if (name == "toggle-state" && item.ToggleType != MenuItemToggleType.None) - return VariantValue.Int32(item.IsChecked ? 1 : 0); + return new DBusVariant(item.IsChecked ? 1 : 0); if (name == "icon-data") { @@ -280,7 +313,7 @@ namespace Avalonia.FreeDesktop var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); using var ms = new MemoryStream(); icon.Save(ms); - return VariantValue.Array(ms.ToArray()); + return new DBusVariant(new List(ms.ToArray())); } } } @@ -288,7 +321,7 @@ namespace Avalonia.FreeDesktop if (name == "children-display") { if (menu is not null) - return VariantValue.String("submenu"); + return new DBusVariant("submenu"); return null; } } @@ -296,40 +329,37 @@ namespace Avalonia.FreeDesktop return null; } - private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names.Length == 0) names = s_allProperties; - var properties = new Dictionary(); + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); - if (v.HasValue) - properties.Add(n, v.Value); + if (v is not null) + properties.Add(n, v); } return properties; } - private (int, Dictionary, VariantValue[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) + private MenuLayout GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = depth == 0 || menu is null ? [] : new VariantValue[menu.Items.Count]; - if (menu is not null) + var children = depth == 0 || menu is null ? new List() : new List(menu.Items.Count); + if (menu is not null && depth != 0) { - for (var c = 0; c < children.Length; c++) + for (var c = 0; c < menu.Items.Count; c++) { var ch = menu.Items[c]; var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); - children[c] = VariantValue.Struct( - VariantValue.Int32(layout.Item1), - new Dict(layout.Item2).AsVariantValue(), - VariantValue.ArrayOfVariant(layout.Item3)); + children.Add(new DBusVariant(layout.ToDbusStruct())); } } - return (id, props, children); + return new MenuLayout(id, props, children); } private void HandleEvent(int id, string eventId) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index b8ed5d30b4..448a6115dc 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,12 +1,13 @@ -using System; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Avalonia.Controls.Platform; +using Avalonia.DBus; +using Avalonia.FreeDesktop.DBusXml; using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Threading; -using Tmds.DBus.Protocol; -using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { @@ -15,11 +16,11 @@ namespace Avalonia.FreeDesktop private static int s_trayIconInstanceId; public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, [255, 0, 0, 0]); - private readonly Connection? _connection; + private readonly DBusConnection? _connection; private readonly OrgFreedesktopDBusProxy? _dBus; private IDisposable? _serviceWatchDisposable; - private readonly PathHandler _pathHandler = new("/StatusNotifierItem"); + private IDisposable? _registration; private readonly StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private OrgKdeStatusNotifierWatcherProxy? _statusNotifierWatcher; private (int, int, byte[]) _icon; @@ -50,24 +51,41 @@ namespace Avalonia.FreeDesktop IsActive = true; - _dBus = new OrgFreedesktopDBusProxy(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); + _dBus = new OrgFreedesktopDBusProxy(_connection, "org.freedesktop.DBus", new DBusObjectPath("/org/freedesktop/DBus")); var dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusMenuPath, _connection); _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath); - _pathHandler.Add(_statusNotifierItemDbusObj); - _connection.AddMethodHandler(_pathHandler); _statusNotifierItemDbusObj.ActivationDelegate += () => OnClicked?.Invoke(); - WatchAsync(); + _ = RegisterAndWatchAsync(); } - private async void WatchAsync() + private async Task RegisterAndWatchAsync() { try { - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item1, x.Item3)); + _registration = await _connection!.RegisterObjects( + (DBusObjectPath)"/StatusNotifierItem", + new object[] { _statusNotifierItemDbusObj! }); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Failed to register StatusNotifierItem handler: {Exception}", e); + return; + } + + await WatchAsync(); + } + + private async Task WatchAsync() + { + try + { + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync( + (name, _, newOwner) => OnNameChange(name, newOwner)); var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); OnNameChange("org.kde.StatusNotifierWatcher", nameOwner); } @@ -87,7 +105,7 @@ namespace Avalonia.FreeDesktop if (!_serviceConnected && newOwner is not null) { _serviceConnected = true; - _statusNotifierWatcher = new OrgKdeStatusNotifierWatcherProxy(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcherProxy(_connection, "org.kde.StatusNotifierWatcher", new DBusObjectPath("/StatusNotifierWatcher")); DestroyTrayIcon(); @@ -113,15 +131,23 @@ namespace Avalonia.FreeDesktop #endif var tid = s_trayIconInstanceId++; - // make sure not to add the path handle and connection method handler twice - if (_statusNotifierItemDbusObj!.PathHandler is null) - _pathHandler.Add(_statusNotifierItemDbusObj!); - - _connection.RemoveMethodHandler(_pathHandler.Path); - _connection.AddMethodHandler(_pathHandler); + // Re-register the handler object if needed + _registration?.Dispose(); + try + { + _registration = await _connection.RegisterObjects( + (DBusObjectPath)"/StatusNotifierItem", + new object[] { _statusNotifierItemDbusObj! }); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Failed to register StatusNotifierItem handler: {Exception}", e); + return; + } _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); - await _dBus!.RequestNameAsync(_sysTrayServiceName, 0); + await _connection.RequestNameAsync(_sysTrayServiceName); await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); _statusNotifierItemDbusObj!.SetTitleAndTooltip(_tooltipText); @@ -133,9 +159,9 @@ namespace Avalonia.FreeDesktop if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _dBus!.ReleaseNameAsync(_sysTrayServiceName); - _pathHandler.Remove(_statusNotifierItemDbusObj); - _connection.RemoveMethodHandler(_pathHandler.Path); + _connection.ReleaseNameAsync(_sysTrayServiceName); + _registration?.Dispose(); + _registration = null; } public void Dispose() @@ -220,43 +246,74 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItemHandler + internal class StatusNotifierItemDbusObj : IOrgKdeStatusNotifierItem { - public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) + private const string InterfaceName = "org.kde.StatusNotifierItem"; + private readonly DBusConnection _connection; + + public StatusNotifierItemDbusObj(DBusConnection connection, string dbusMenuPath) { - Connection = connection; - Menu = dbusMenuPath; + _connection = connection; + Menu = (DBusObjectPath)dbusMenuPath; } - public override Connection Connection { get; } - public event Action? ActivationDelegate; - protected override ValueTask OnContextMenuAsync(Message message, int x, int y) => new(); - - protected override ValueTask OnActivateAsync(Message message, int x, int y) + // IOrgKdeStatusNotifierItem properties + public string Category { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public int WindowId { get; } = 0; + public string IconThemePath { get; } = string.Empty; + public DBusObjectPath Menu { get; } + public bool ItemIsMenu { get; } = false; + public string IconName { get; } = string.Empty; + public List IconPixmap { get; set; } = []; + public string OverlayIconName { get; } = string.Empty; + public List OverlayIconPixmap { get; } = []; + public string AttentionIconName { get; } = string.Empty; + public List AttentionIconPixmap { get; } = []; + public string AttentionMovieName { get; } = string.Empty; + public ToolTip ToolTip { get; set; } = new(string.Empty, [], string.Empty, string.Empty); + + // IOrgKdeStatusNotifierItem methods + public ValueTask ContextMenuAsync(int x, int y) => new(); + + public ValueTask ActivateAsync(int x, int y) { ActivationDelegate?.Invoke(); return new ValueTask(); } - protected override ValueTask OnSecondaryActivateAsync(Message message, int x, int y) => new(); + public ValueTask SecondaryActivateAsync(int x, int y) => new(); - protected override ValueTask OnScrollAsync(Message message, int delta, string orientation) => new(); + public ValueTask ScrollAsync(int delta, string orientation) => new(); + + // Signal emission helpers + private void EmitSignal(string signalName, params object[] body) + { + var message = DBusMessage.CreateSignal( + (DBusObjectPath)"/StatusNotifierItem", + InterfaceName, + signalName, + body); + _ = _connection.SendMessageAsync(message); + } public void InvalidateAll() { - EmitNewTitle(); - EmitNewIcon(); - EmitNewAttentionIcon(); - EmitNewOverlayIcon(); - EmitNewToolTip(); - EmitNewStatus(Status); + EmitSignal("NewTitle"); + EmitSignal("NewIcon"); + EmitSignal("NewAttentionIcon"); + EmitSignal("NewOverlayIcon"); + EmitSignal("NewToolTip"); + EmitSignal("NewStatus", Status); } public void SetIcon((int, int, byte[]) dbusPixmap) { - IconPixmap = [dbusPixmap]; + IconPixmap = [new IconPixmap(dbusPixmap.Item1, dbusPixmap.Item2, new List(dbusPixmap.Item3))]; InvalidateAll(); }