Browse Source

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.
pull/20799/head
Jumar Macato 3 weeks ago
parent
commit
d3e773ab0b
  1. 26
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  2. 53
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  3. 152
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  4. 137
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

26
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<IDisposable> WatchCommitStringAsync(Action<Exception?, string> handler) =>
public Task<IDisposable> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? new ValueTask<IDisposable>(Disposable.Empty);
?? Task.FromResult<IDisposable>(Disposable.Empty);
public ValueTask<IDisposable> WatchForwardKeyAsync(Action<Exception?, (uint keyval, uint state, int type)> handler) =>
public Task<IDisposable> WatchForwardKeyAsync(Action<uint, uint, int> handler) =>
_old?.WatchForwardKeyAsync(handler)
?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0)))
?? new ValueTask<IDisposable>(Disposable.Empty);
?? _modern?.WatchForwardKeyAsync((keyval, state, type) => handler.Invoke(keyval, state, type ? 1 : 0))
?? Task.FromResult<IDisposable>(Disposable.Empty);
public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync(
Action<Exception?, ((string?, int)[]? str, int cursorpos)> handler) =>
_old?.WatchUpdateFormattedPreeditAsync(handler!)
?? _modern?.WatchUpdateFormattedPreeditAsync(handler!)
?? new ValueTask<IDisposable>(Disposable.Empty);
public Task<IDisposable> WatchUpdateFormattedPreeditAsync(
Action<List<FormattedPreeditSegment>, int> handler) =>
_old?.WatchUpdateFormattedPreeditAsync(handler)
?? _modern?.WatchUpdateFormattedPreeditAsync(handler)
?? Task.FromResult<IDisposable>(Disposable.Empty);
public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync((ulong)flags) ?? Task.CompletedTask;
}
}

53
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<bool> 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<InputContextArg> { 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<FormattedPreeditSegment> 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);
}
}

152
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<int, NativeMenuItemBase> _idsToItems = new();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new();
private readonly HashSet<NativeMenu> _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<string> IconThemePath { get; } = [];
protected override ValueTask<(uint Revision, (int, Dictionary<string, VariantValue>, 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<string> 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<string, VariantValue>, VariantValue[]))>((_revision, layout));
return new ValueTask<(uint, MenuLayout)>((_revision, layout));
}
protected override ValueTask<(int, Dictionary<string, VariantValue>)[]> OnGetGroupPropertiesAsync(Message message, int[] ids, string[] propertyNames)
=> new(ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray());
public ValueTask<List<MenuItemProperties>> GetGroupPropertiesAsync(List<int> ids, List<string> propertyNames)
{
var names = propertyNames?.ToArray() ?? [];
var result = ids.Select(id => new MenuItemProperties(id, GetProperties(GetMenu(id), names))).ToList();
return new ValueTask<List<MenuItemProperties>>(result);
}
protected override ValueTask<VariantValue> OnGetPropertyAsync(Message message, int id, string name) =>
new(GetProperty(GetMenu(id), name) ?? VariantValue.Int32(0));
public ValueTask<DBusVariant> 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<int[]> OnEventGroupAsync(Message message, (int, string, VariantValue, uint)[] events)
public ValueTask<List<int>> EventGroupAsync(List<MenuEvent> events)
{
foreach (var e in events)
HandleEvent(e.Item1, e.Item2);
return new ValueTask<int[]>([]);
HandleEvent(e.Id, e.EventId);
return new ValueTask<List<int>>([]);
}
protected override ValueTask<bool> OnAboutToShowAsync(Message message, int id) => new(false);
public ValueTask<bool> AboutToShowAsync(int id) => new(false);
protected override ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> OnAboutToShowGroupAsync(Message message, int[] ids) =>
new(([], []));
public ValueTask<(List<int> UpdatesNeeded, List<int> IdErrors)> AboutToShowGroupAsync(List<int> ids) =>
new((new List<int>(), new List<int>()));
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 ?? "<null>");
return new DBusVariant(item.Header ?? "<null>");
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<string>();
var lst = new List<string>();
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<Array<string>> { lst }.AsVariantValue();
return new DBusVariant(new List<List<string>> { 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<byte>(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<string, VariantValue> GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
private static Dictionary<string, DBusVariant> GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
{
if (names.Length == 0)
names = s_allProperties;
var properties = new Dictionary<string, VariantValue>();
var properties = new Dictionary<string, DBusVariant>();
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<string, VariantValue>, 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<DBusVariant>() : new List<DBusVariant>(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<string, VariantValue>(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)

137
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
/// <remarks>
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
/// </remarks>
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> IconPixmap { get; set; } = [];
public string OverlayIconName { get; } = string.Empty;
public List<IconPixmap> OverlayIconPixmap { get; } = [];
public string AttentionIconName { get; } = string.Empty;
public List<IconPixmap> 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<byte>(dbusPixmap.Item3))];
InvalidateAll();
}

Loading…
Cancel
Save