Browse Source

Update to latest Tmds.DBus.Protocol. (#21472)

Change to the latest version of Tmds.DBus.Protocol and use the project source generator.

Fix StatusNotifierItem Status property to use "Active" value.
Fix "watch after request" race for portal requests.

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/21486/head
Tom Deseyn 4 months ago
committed by GitHub
parent
commit
5ad8b75427
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      Directory.Packages.props
  2. 34
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  3. 11
      src/Avalonia.FreeDesktop/DBusHelper.cs
  4. 85
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  5. 18
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  6. 25
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  7. 40
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  8. 5
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  9. 60
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  10. 22
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  11. 98
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  12. 147
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

4
Directory.Packages.props

@ -61,8 +61,8 @@
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.92.0" />
<PackageVersion Include="Tmds.DBus.SourceGenerator" Version="0.0.22" />
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
<PackageVersion Include="Tmds.DBus.Generator" Version="0.94.1" />
<PackageVersion Include="Xamarin.AndroidX.AppCompat" Version="1.7.1.3" />
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.2.0.2" />
<PackageVersion Include="Xamarin.AndroidX.Window" Version="1.5.1.2" />

34
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" />
<PackageReference Include="Tmds.DBus.SourceGenerator" PrivateAssets="all" />
<PackageReference Include="Tmds.DBus.Generator" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
@ -27,22 +27,22 @@
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="DBusXml/com.canonical.AppMenu.Registrar.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/com.canonical.dbusmenu.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext1.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod1.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.DBus.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.IBus.Portal.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.FileChooser.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Request.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Settings.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.kde.StatusNotifierWatcher.xml" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.kde.StatusNotifierItem.xml" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/com.canonical.AppMenu.Registrar.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/com.canonical.dbusmenu.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Handler" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext.xml" Namespace="Avalonia.FreeDesktop.DBusIme.Fcitx.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod.xml" Namespace="Avalonia.FreeDesktop.DBusIme.Fcitx.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputContext1.xml" Namespace="Avalonia.FreeDesktop.DBusIme.Fcitx.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.fcitx.Fcitx.InputMethod1.xml" Namespace="Avalonia.FreeDesktop.DBusIme.Fcitx.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.DBus.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.IBus.Portal.xml" Namespace="Avalonia.FreeDesktop.DBusIme.IBus.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.FileChooser.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Request.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.freedesktop.portal.Settings.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.kde.StatusNotifierWatcher.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Proxy" />
<AdditionalFiles Include="DBusXml/org.kde.StatusNotifierItem.xml" Namespace="Avalonia.FreeDesktop.DBus" DBusGeneratorMode="Handler" />
</ItemGroup>
</Project>

11
src/Avalonia.FreeDesktop/DBusHelper.cs

@ -1,4 +1,3 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Threading;
using Avalonia.Logging;
@ -8,9 +7,9 @@ namespace Avalonia.FreeDesktop
{
internal static class DBusHelper
{
private static Connection? s_defaultConntection;
private static DBusConnection? s_defaultConntection;
private static bool s_defaultConnectionFailed;
public static Connection? DefaultConnection
public static DBusConnection? DefaultConnection
{
get
{
@ -25,14 +24,14 @@ namespace Avalonia.FreeDesktop
}
}
public static Connection? TryCreateNewConnection(string? dbusAddress = null)
public static DBusConnection? TryCreateNewConnection(string? dbusAddress = null)
{
var oldContext = SynchronizationContext.Current;
Connection? conn = null;
DBusConnection? conn = null;
try
{
SynchronizationContext.SetSynchronizationContext(null);
conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!)
conn = new DBusConnection(new DBusConnectionOptions(dbusAddress ?? DBusAddress.Session!)
{
AutoConnect = false,
});

85
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -1,14 +1,13 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Logging;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
namespace Avalonia.FreeDesktop.DBusIme
{
@ -32,8 +31,9 @@ namespace Avalonia.FreeDesktop.DBusIme
{
private List<IDisposable> _disposables = new List<IDisposable>();
private Queue<string> _onlineNamesQueue = new Queue<string>();
protected Connection Connection { get; }
protected DBusConnection Connection { get; }
private readonly string[] _knownNames;
private CancellationTokenSource? _watchCts;
private bool _connecting;
private string? _currentName;
private DBusCallQueue _queue;
@ -47,39 +47,62 @@ namespace Avalonia.FreeDesktop.DBusIme
protected bool IsConnected => _currentName != null;
public DBusTextInputMethodBase(Connection connection, params string[] knownNames)
public DBusTextInputMethodBase(DBusConnection connection, params string[] knownNames)
{
_queue = new DBusCallQueue(QueueOnErrorAsync);
Connection = connection;
_knownNames = knownNames;
_ = WatchAsync();
WatchAsync();
}
public TextInputMethodClient? Client => _client;
public bool IsActive => _client is not null;
private async Task WatchAsync()
private void WatchAsync()
{
var dbus = new OrgFreedesktopDBusProxy(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
try
{
_disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange));
}
catch (DBusException e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, $"WatchNameOwnerChangedAsync failed: {e}");
}
_watchCts = new CancellationTokenSource();
var cancellationToken = _watchCts.Token;
foreach (var name in _knownNames)
WatchNameAsync(name);
async void WatchNameAsync(string name)
{
try
{
var nameOwner = await dbus.GetNameOwnerAsync(name);
OnNameChange(null, (name, null, nameOwner));
using var watcher = await Connection.WatchNameOwnerAsync(name);
var owner = watcher.GetCurrentOwner();
OnOwnerChanged(name, owner);
while (!cancellationToken.IsCancellationRequested)
{
if (owner is not null)
{
var ct = watcher.GetOwnerChangedCancellationToken(owner);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationToken);
try
{
await Task.Delay(Timeout.Infinite, linked.Token);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{ }
}
else
{
try
{
await watcher.WaitForOwnerAsync(cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{ }
}
owner = watcher.GetCurrentOwner();
OnOwnerChanged(name, owner);
}
}
catch (DBusException e)
catch (Exception e) when (!cancellationToken.IsCancellationRequested)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, $"GetNameOwnerAsync failed: {e}");
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)
?.Log(this, $"WatchNameOwner for '{name}' failed: {e}");
}
}
}
@ -89,22 +112,11 @@ namespace Avalonia.FreeDesktop.DBusIme
protected string GetAppName() =>
Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia";
private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args)
private async void OnOwnerChanged(string serviceName, string? newOwner)
{
if (e is not null)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, $"OnNameChange failed: {e}");
return;
}
if (!_knownNames.Contains(args.ServiceName))
if (newOwner is not null && _currentName is null)
{
return;
}
if (args.NewOwner is not null && _currentName is null)
{
_onlineNamesQueue.Enqueue(args.ServiceName);
_onlineNamesQueue.Enqueue(serviceName);
if (!_connecting)
{
_connecting = true;
@ -134,11 +146,10 @@ namespace Avalonia.FreeDesktop.DBusIme
_connecting = false;
}
}
}
// IME has crashed
if (args.NewOwner is null && args.ServiceName == _currentName)
if (newOwner is null && serviceName == _currentName)
{
_currentName = null;
foreach (var s in _disposables)
@ -147,9 +158,6 @@ namespace Avalonia.FreeDesktop.DBusIme
OnDisconnected();
Reset();
// Watch again
_ = WatchAsync();
}
}
@ -196,6 +204,7 @@ namespace Avalonia.FreeDesktop.DBusIme
public async void Dispose()
{
_watchCts?.Cancel();
foreach(var d in _disposables)
d.Dispose();
_disposables.Clear();

18
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs

@ -1,21 +1,21 @@
using System;
using System.Threading.Tasks;
using Avalonia.Reactive;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBusIme.Fcitx.DBus;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
internal class FcitxICWrapper
{
private readonly OrgFcitxFcitxInputContext1Proxy? _modern;
private readonly OrgFcitxFcitxInputContextProxy? _old;
private readonly InputContext1? _modern;
private readonly InputContext? _old;
public FcitxICWrapper(OrgFcitxFcitxInputContextProxy old)
public FcitxICWrapper(InputContext old)
{
_old = old;
}
public FcitxICWrapper(OrgFcitxFcitxInputContext1Proxy modern)
public FcitxICWrapper(InputContext1 modern)
{
_modern = modern;
}
@ -38,18 +38,18 @@ 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 ValueTask<IDisposable> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? new ValueTask<IDisposable>(Disposable.Empty);
public ValueTask<IDisposable> WatchForwardKeyAsync(Action<Exception?, (uint keyval, uint state, int type)> handler) =>
public ValueTask<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) =>
_old?.WatchForwardKeyAsync(handler)
?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0)))
?? _modern?.WatchForwardKeyAsync(ev => handler.Invoke((ev.Keyval, ev.State, ev.Type ? 1 : 0)))
?? new ValueTask<IDisposable>(Disposable.Empty);
public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync(
Action<Exception?, ((string?, int)[]? str, int cursorpos)> handler) =>
Action<((string?, int)[]? str, int cursorpos)> handler) =>
_old?.WatchUpdateFormattedPreeditAsync(handler!)
?? _modern?.WatchUpdateFormattedPreeditAsync(handler!)
?? new ValueTask<IDisposable>(Disposable.Empty);

25
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@ -1,4 +1,3 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Diagnostics;
using System.Linq;
@ -9,7 +8,7 @@ using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Logging;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBusIme.Fcitx.DBus;
namespace Avalonia.FreeDesktop.DBusIme.Fcitx
{
@ -20,24 +19,24 @@ 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 DBus.InputMethod(Connection, name, "/inputmethod");
var resp = await method.CreateICv3Async(GetAppName(),
Process.GetCurrentProcess().Id);
var proxy = new OrgFcitxFcitxInputContextProxy(Connection, name, $"/inputcontext_{resp.Icid}");
var proxy = new InputContext(Connection, name, $"/inputcontext_{resp.Icid}");
_context = new FcitxICWrapper(proxy);
}
else
{
var method = new OrgFcitxFcitxInputMethod1Proxy(Connection, name, "/inputmethod");
var method = new InputMethod1(Connection, name, "/inputmethod");
var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
var proxy = new OrgFcitxFcitxInputContext1Proxy(Connection, name, resp.Item1);
var proxy = new InputContext1(Connection, name, resp.Item1);
_context = new FcitxICWrapper(proxy);
}
@ -47,7 +46,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
return true;
}
private void OnPreedit(Exception? arg1, ((string?, int)[]? str, int cursorpos) args)
private void OnPreedit(((string?, int)[]? str, int cursorpos) args)
{
int? cursor = null;
string? preeditString = null;
@ -169,7 +168,7 @@ 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) ev)
{
var state = (FcitxKeyState)ev.state;
KeyModifiers mods = default;
@ -193,14 +192,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);
}
}

40
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@ -1,4 +1,3 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Threading.Tasks;
using Avalonia.Input;
@ -7,27 +6,27 @@ using Avalonia.Input.TextInput;
using Avalonia.Logging;
using Avalonia.Media.TextFormatting.Unicode;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBusIme.IBus.DBus;
namespace Avalonia.FreeDesktop.DBusIme.IBus
{
internal class IBusX11TextInputMethod : DBusTextInputMethodBase
{
private OrgFreedesktopIBusServiceProxy? _service;
private OrgFreedesktopIBusInputContextProxy? _context;
private Service? _service;
private InputContext? _context;
private string _preeditText = "";
private int _preeditCursor;
private int _insideReset;
public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { }
public IBusX11TextInputMethod(DBusConnection connection) : base(connection, "org.freedesktop.portal.IBus") { }
protected override async Task<bool> Connect(string name)
{
var portal = new OrgFreedesktopIBusPortalProxy(Connection, name, "/org/freedesktop/IBus");
var portal = new Portal(Connection, name, "/org/freedesktop/IBus");
var path = await portal.CreateInputContextAsync(GetAppName());
_service = new OrgFreedesktopIBusServiceProxy(Connection, name, path);
_context = new OrgFreedesktopIBusInputContextProxy(Connection, name, path);
_service = new Service(Connection, name, path);
_context = new InputContext(Connection, name, path);
AddDisposable(await _context.WatchCommitTextAsync(OnCommitText));
AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey));
AddDisposable(await _context.WatchUpdatePreeditTextAsync(OnUpdatePreedit));
@ -37,23 +36,23 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
return true;
}
private void OnHidePreedit(Exception? obj)
private void OnHidePreedit()
{
if (Client?.SupportsPreedit != true || string.IsNullOrEmpty(_preeditText))
{
return;
}
_preeditText = "";
Client?.SetPreeditText(_preeditText, 0);
}
private void OnShowPreedit(Exception? obj)
private void OnShowPreedit()
{
}
private void OnUpdatePreedit(Exception? arg1, (VariantValue Text, uint CursorPos, bool Visible) preeditComponents)
private void OnUpdatePreedit((VariantValue Text, uint CursorPos, bool Visible) preeditComponents)
{
string? preeditText;
@ -81,14 +80,8 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
Client.SetPreeditText(_preeditText, _preeditCursor);
}
private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k)
private void OnForwardKey((uint keyval, uint keycode, uint state) k)
{
if (e is not null)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, $"OnForwardKey failed: {e}");
return;
}
var state = (IBusModifierMask)k.state;
KeyModifiers mods = default;
if (state.HasAllFlags(IBusModifierMask.ControlMask))
@ -107,7 +100,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
});
}
private void OnCommitText(Exception? e, VariantValue variantItem)
private void OnCommitText(VariantValue variantItem)
{
if (_insideReset > 0)
{
@ -117,11 +110,6 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
// check if we have any pending Reset calls and ignore the signal here
return;
}
if (e is not null)
{
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, $"OnCommitText failed: {e}");
return;
}
if (variantItem.Count >= 3 && variantItem.GetItem(2) is { Type: VariantValueType.String } stringItem)
FireCommit(stringItem.GetString());

5
src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs

@ -1,4 +1,3 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Collections.Generic;
using Avalonia.FreeDesktop.DBusIme.Fcitx;
@ -9,7 +8,7 @@ namespace Avalonia.FreeDesktop.DBusIme
{
internal class X11DBusImeHelper
{
private static readonly Dictionary<string, Func<Connection, IX11InputMethodFactory>> KnownMethods = new()
private static readonly Dictionary<string, Func<DBusConnection, IX11InputMethodFactory>> KnownMethods = new()
{
["fcitx"] = static conn =>
new DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)),
@ -19,7 +18,7 @@ namespace Avalonia.FreeDesktop.DBusIme
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
};
private static Func<Connection, IX11InputMethodFactory>? DetectInputMethod()
private static Func<DBusConnection, IX11InputMethodFactory>? DetectInputMethod()
{
foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" })
{

60
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -1,4 +1,3 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
@ -12,7 +11,7 @@ using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.Logging;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBus;
namespace Avalonia.FreeDesktop
{
@ -21,51 +20,53 @@ namespace Avalonia.FreeDesktop
public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) =>
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 : DBusHandler, IdbusmenuHandler, IdbusmenuProperties, ITopLevelNativeMenuExporter, IDisposable
{
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 uint _xid;
private readonly bool _appMenu = true;
private ComCanonicalAppMenuRegistrarProxy? _registrar;
private Registrar? _registrar;
private NativeMenu? _menu;
private bool _disposed;
private uint _revision = 1;
private bool _resetQueued;
private int _nextId = 1;
public DBusMenuExporterImpl(Connection connection, IntPtr xid)
public DBusMenuExporterImpl(DBusConnection connection, IntPtr xid)
: base(connection, GenerateDBusMenuObjPath, handlesChildPaths: false)
{
Version = 4;
Connection = connection;
_xid = (uint)xid.ToInt32();
_pathHandler = new PathHandler(GenerateDBusMenuObjPath);
_pathHandler.Add(this);
SetNativeMenu([]);
_ = InitializeAsync();
}
public DBusMenuExporterImpl(Connection connection, string path)
public DBusMenuExporterImpl(DBusConnection connection, string path)
: base(connection, path, handlesChildPaths: false)
{
Version = 4;
Connection = connection;
_appMenu = false;
_pathHandler = new PathHandler(path);
_pathHandler.Add(this);
SetNativeMenu([]);
_ = InitializeAsync();
}
public override Connection Connection { get; }
uint IdbusmenuProperties.Version => 4;
string IdbusmenuProperties.TextDirection => "ltr";
string IdbusmenuProperties.Status => "normal";
string[] IdbusmenuProperties.IconThemePath => [];
protected override ValueTask<(uint Revision, (int, Dictionary<string, VariantValue>, VariantValue[]) Layout)> OnGetLayoutAsync(Message message, int parentId, int recursionDepth, string[] propertyNames)
ValueTask IdbusmenuHandler.HandleGetPropertyAsync(IdbusmenuHandler.GetPropertyContext context)
=> context.Handle(this);
ValueTask IdbusmenuHandler.HandleGetAllPropertiesAsync(IdbusmenuHandler.GetAllPropertiesContext context)
=> context.Handle(this);
ValueTask<(uint Revision, (int, Dictionary<string, VariantValue>, VariantValue[]) Layout)> IdbusmenuHandler.GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames)
{
var menu = GetMenu(parentId);
var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames);
@ -78,41 +79,41 @@ namespace Avalonia.FreeDesktop
return new ValueTask<(uint, (int, Dictionary<string, VariantValue>, VariantValue[]))>((_revision, layout));
}
protected override ValueTask<(int, Dictionary<string, VariantValue>)[]> OnGetGroupPropertiesAsync(Message message, int[] ids, string[] propertyNames)
ValueTask<(int, Dictionary<string, VariantValue>)[]> IdbusmenuHandler.GetGroupPropertiesAsync(int[] ids, string[] propertyNames)
=> new(ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray());
protected override ValueTask<VariantValue> OnGetPropertyAsync(Message message, int id, string name) =>
ValueTask<VariantValue> IdbusmenuHandler.GetPropertyAsync(int id, string name) =>
new(GetProperty(GetMenu(id), name) ?? VariantValue.Int32(0));
protected override ValueTask OnEventAsync(Message message, int id, string eventId, VariantValue data, uint timestamp)
ValueTask IdbusmenuHandler.EventAsync(int id, string eventId, VariantValue data, uint timestamp)
{
HandleEvent(id, eventId);
return new ValueTask();
}
protected override ValueTask<int[]> OnEventGroupAsync(Message message, (int, string, VariantValue, uint)[] events)
ValueTask<int[]> IdbusmenuHandler.EventGroupAsync((int, string, VariantValue, uint)[] events)
{
foreach (var e in events)
HandleEvent(e.Item1, e.Item2);
return new ValueTask<int[]>([]);
}
protected override ValueTask<bool> OnAboutToShowAsync(Message message, int id) => new(false);
ValueTask<bool> IdbusmenuHandler.AboutToShowAsync(int id) => new(false);
protected override ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> OnAboutToShowGroupAsync(Message message, int[] ids) =>
ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> IdbusmenuHandler.AboutToShowGroupAsync(int[] ids) =>
new(([], []));
private async Task InitializeAsync()
{
Connection.AddMethodHandler(_pathHandler);
Connection.AddMethodHandler(this);
if (!_appMenu)
return;
_registrar = new ComCanonicalAppMenuRegistrarProxy(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar");
_registrar = new Registrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar");
try
{
if (!_disposed)
await _registrar.RegisterWindowAsync(_xid, _pathHandler.Path);
await _registrar.RegisterWindowAsync(_xid, Path);
}
catch
{
@ -138,8 +139,7 @@ namespace Avalonia.FreeDesktop
?.Log(this, "DBusMenu UnregisterWindowAsync failed: {Exception}", t.Exception);
}
}, TaskContinuationOptions.OnlyOnFaulted);
_pathHandler.Remove(this);
Connection.RemoveMethodHandler(_pathHandler.Path);
Connection.RemoveMethodHandler(Path);
}
public bool IsNativeMenuExported { get; private set; }
@ -176,7 +176,7 @@ namespace Avalonia.FreeDesktop
_idsToItems.Clear();
_itemsToIds.Clear();
_revision++;
EmitLayoutUpdated(_revision, 0);
Connection.EmitLayoutUpdated(Path, _revision, 0);
}
private void QueueReset()

22
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -1,17 +1,16 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBus;
namespace Avalonia.FreeDesktop
{
internal class DBusPlatformSettings : DefaultPlatformSettings
{
private readonly OrgFreedesktopPortalSettingsProxy? _settings;
private readonly Settings? _settings;
private PlatformColorValues? _lastColorValues;
private PlatformThemeVariant? _themeVariant;
@ -22,7 +21,7 @@ namespace Avalonia.FreeDesktop
if (DBusHelper.DefaultConnection is not { } conn)
return;
_settings = new OrgFreedesktopPortalSettingsProxy(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_settings = new Settings(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
_ = TryGetInitialValuesAsync();
}
@ -42,7 +41,7 @@ namespace Avalonia.FreeDesktop
{
try
{
var version = await _settings!.GetVersionPropertyAsync();
var version = await _settings!.GetVersionAsync();
VariantValue value;
if (version >= 2)
value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "color-scheme");
@ -51,7 +50,7 @@ namespace Avalonia.FreeDesktop
value = (await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme")).GetVariantValue();
return ToColorScheme(value.GetUInt32());
}
catch (DBusException)
catch (DBusExceptionBase)
{
return null;
}
@ -61,7 +60,7 @@ namespace Avalonia.FreeDesktop
{
try
{
var version = await _settings!.GetVersionPropertyAsync();
var version = await _settings!.GetVersionAsync();
VariantValue value;
if (version >= 2)
value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "accent-color");
@ -69,17 +68,14 @@ namespace Avalonia.FreeDesktop
value = await _settings!.ReadAsync("org.freedesktop.appearance", "accent-color");
return ToAccentColor(value);
}
catch (DBusException)
catch (DBusExceptionBase)
{
return null;
}
}
private void SettingsChangedHandler(Exception? exception, (string Namespace, string Key, VariantValue Value) tuple)
private void SettingsChangedHandler((string Namespace, string Key, VariantValue Value) tuple)
{
if (exception is not null)
return;
switch (tuple)
{
case ("org.freedesktop.appearance", "color-scheme", var colorScheme):

98
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -1,6 +1,6 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
@ -10,7 +10,7 @@ using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Threading;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBus;
namespace Avalonia.FreeDesktop
{
@ -32,12 +32,12 @@ namespace Avalonia.FreeDesktop
using var restoreContext = AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Input);
var dbusFileChooser = new OrgFreedesktopPortalFileChooserProxy(conn, "org.freedesktop.portal.Desktop",
var dbusFileChooser = new FileChooser(conn, "org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop");
uint version;
try
{
version = await dbusFileChooser.GetVersionPropertyAsync();
version = await dbusFileChooser.GetVersionAsync();
}
catch
{
@ -47,13 +47,13 @@ namespace Avalonia.FreeDesktop
return new DBusSystemDialog(conn, parentLeaseProvider, dbusFileChooser, version);
}
private readonly Connection _connection;
private readonly OrgFreedesktopPortalFileChooserProxy _fileChooser;
private readonly DBusConnection _connection;
private readonly FileChooser _fileChooser;
private readonly Func<Task<IPortalParentLease?>>? _parentLeaseProvider;
private readonly uint _version;
private DBusSystemDialog(Connection connection, Func<Task<IPortalParentLease?>>? parentLeaseProvider,
OrgFreedesktopPortalFileChooserProxy fileChooser, uint version)
private DBusSystemDialog(DBusConnection connection, Func<Task<IPortalParentLease?>>? parentLeaseProvider,
FileChooser fileChooser, uint version)
{
_connection = connection;
_fileChooser = fileChooser;
@ -74,7 +74,7 @@ namespace Avalonia.FreeDesktop
{
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
ObjectPath objectPath;
var (expectedPath, token) = CreateRequestToken();
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeFilter, options.SuggestedFileType, out var filters,
@ -89,19 +89,20 @@ namespace Avalonia.FreeDesktop
chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0")));
chooserOptions.Add("multiple", VariantValue.Bool(options.AllowMultiple));
chooserOptions.Add("handle_token", token);
objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request =
new OrgFreedesktopPortalRequestProxy(_connection, "org.freedesktop.portal.Desktop", objectPath);
var request = new Request(_connection, "org.freedesktop.portal.Desktop", expectedPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync((e, x) =>
using var disposable = await request.WatchResponseAsync(notification =>
{
if (e is not null)
tsc.TrySetException(e);
if (notification.IsCompletion)
tsc.TrySetException(notification.Exception);
else
tsc.TrySetResult(x.Results["uris"].GetArray<string>());
});
tsc.TrySetResult(notification.Value.Results["uris"].GetArray<string>());
}, ObserverFlags.EmitAll);
var actualPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
VerifyRequestPath(tsc, expectedPath, actualPath);
var uris = await tsc.Task ?? [];
return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
@ -124,7 +125,7 @@ namespace Avalonia.FreeDesktop
{
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
ObjectPath objectPath;
var (expectedPath, token) = CreateRequestToken();
var chooserOptions = new Dictionary<string, VariantValue>();
if (TryParseFilters(options.FileTypeChoices, options.SuggestedFileType, out var filters,
out var currentFilter))
@ -138,21 +139,20 @@ namespace Avalonia.FreeDesktop
chooserOptions.Add("current_name", VariantValue.String(currentName));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0")));
chooserOptions.Add("handle_token", token);
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions)
.ConfigureAwait(false);
var request =
new OrgFreedesktopPortalRequestProxy(_connection, "org.freedesktop.portal.Desktop", objectPath);
var request = new Request(_connection, "org.freedesktop.portal.Desktop", expectedPath);
var tsc = new TaskCompletionSource<string[]?>();
FilePickerFileType? selectedType = null;
using var disposable = await request.WatchResponseAsync((e, x) =>
using var disposable = await request.WatchResponseAsync(notification =>
{
if (e is not null)
if (notification.IsCompletion)
{
tsc.TrySetException(e);
tsc.TrySetException(notification.Exception);
}
else
{
var x = notification.Value;
if (x.Results.TryGetValue("current_filter", out var currentFilter))
{
var name = currentFilter.GetItem(0).GetString();
@ -166,19 +166,23 @@ namespace Avalonia.FreeDesktop
else
patterns.Add(t.GetItem(1).GetString());
}
// Reuse the file type objects from options
// so the consuming code can match exactly the
// file type selected instead of spawning one.
selectedType = options.FileTypeChoices?.FirstOrDefault(type => type.Name == name && (
(type.MimeTypes?.All(y => mimeTypes.Contains(y)) ?? false) ||
(type.Patterns?.All(y => patterns.Contains(y)) ?? false)))
(type.Patterns?.All(y => patterns.Contains(y)) ?? false)))
?? new FilePickerFileType(name) { MimeTypes = mimeTypes, Patterns = patterns };
}
tsc.TrySetResult(x.Results["uris"].GetArray<string>());
}
}).ConfigureAwait(false);
}, ObserverFlags.EmitAll).ConfigureAwait(false);
var actualPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions)
.ConfigureAwait(false);
VerifyRequestPath(tsc, expectedPath, actualPath);
var uris = await tsc.Task.ConfigureAwait(false);
var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null;
@ -198,6 +202,7 @@ namespace Avalonia.FreeDesktop
await using var parentLease = await AcquireParentLeaseAsync().ConfigureAwait(false);
var parentWindow = parentLease?.Handle ?? string.Empty;
var (expectedPath, token) = CreateRequestToken();
var chooserOptions = new Dictionary<string, VariantValue>
{
{ "directory", VariantValue.Bool(true) }, { "multiple", VariantValue.Bool(options.AllowMultiple) }
@ -207,19 +212,20 @@ namespace Avalonia.FreeDesktop
chooserOptions.Add("current_name", VariantValue.String(currentName));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0")));
chooserOptions.Add("handle_token", token);
var objectPath =
await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request =
new OrgFreedesktopPortalRequestProxy(_connection, "org.freedesktop.portal.Desktop", objectPath);
var request = new Request(_connection, "org.freedesktop.portal.Desktop", expectedPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync((e, x) =>
using var disposable = await request.WatchResponseAsync(notification =>
{
if (e is not null)
tsc.TrySetException(e);
if (notification.IsCompletion)
tsc.TrySetException(notification.Exception);
else
tsc.TrySetResult(x.Results["uris"].GetArray<string>());
});
tsc.TrySetResult(notification.Value.Results["uris"].GetArray<string>());
}, ObserverFlags.EmitAll);
var actualPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
VerifyRequestPath(tsc, expectedPath, actualPath);
var uris = await tsc.Task ?? Array.Empty<string>();
return uris
@ -273,5 +279,21 @@ namespace Avalonia.FreeDesktop
result = filters.AsVariantValue();
return true;
}
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
// Subscribe to the Response signal before making the portal call to avoid a race condition.
private (ObjectPath ExpectedPath, string Token) CreateRequestToken()
{
string sender = (_connection.UniqueName ?? "").TrimStart(':').Replace(".", "_");
string token = "Avalonia_" + Stopwatch.GetTimestamp().ToString();
ObjectPath expectedPath = $"/org/freedesktop/portal/desktop/request/{sender}/{token}";
return (expectedPath, token);
}
private static void VerifyRequestPath<T>(TaskCompletionSource<T> tsc, ObjectPath expectedPath, ObjectPath actualPath)
{
if (actualPath != expectedPath)
tsc.TrySetException(new InvalidOperationException($"Portal returned unexpected request path '{actualPath}', expected '{expectedPath}'."));
}
}
}

147
src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@ -1,13 +1,13 @@
#pragma warning disable CS0618 // TODO: Temporary workaround until Tmds is replaced.
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Threading;
using Tmds.DBus.Protocol;
using Tmds.DBus.SourceGenerator;
using Avalonia.FreeDesktop.DBus;
namespace Avalonia.FreeDesktop
{
@ -16,13 +16,10 @@ 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 OrgFreedesktopDBusProxy? _dBus;
private IDisposable? _serviceWatchDisposable;
private readonly PathHandler _pathHandler = new("/StatusNotifierItem");
private readonly DBusConnection? _connection;
private CancellationTokenSource? _watchCts;
private readonly StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private OrgKdeStatusNotifierWatcherProxy? _statusNotifierWatcher;
private StatusNotifierWatcher? _statusNotifierWatcher;
private (int, int, byte[]) _icon;
private string? _sysTrayServiceName;
@ -51,14 +48,12 @@ namespace Avalonia.FreeDesktop
IsActive = true;
_dBus = new OrgFreedesktopDBusProxy(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
var dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusMenuPath, _connection);
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath);
_pathHandler.Add(_statusNotifierItemDbusObj);
_connection.AddMethodHandler(_pathHandler);
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
_statusNotifierItemDbusObj.ActivationDelegate += () => OnClicked?.Invoke();
WatchAsync();
@ -68,27 +63,52 @@ namespace Avalonia.FreeDesktop
{
try
{
_serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item1, x.Item3));
var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher");
OnNameChange("org.kde.StatusNotifierWatcher", nameOwner);
_watchCts = new CancellationTokenSource();
using var watcher = await _connection!.WatchNameOwnerAsync("org.kde.StatusNotifierWatcher");
var owner = watcher.GetCurrentOwner();
OnOwnerChanged(owner);
while (!_watchCts.IsCancellationRequested)
{
if (owner is not null)
{
var ct = watcher.GetOwnerChangedCancellationToken(owner);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, _watchCts.Token);
try
{
await Task.Delay(Timeout.Infinite, linked.Token);
}
catch (OperationCanceledException) when (!_watchCts.IsCancellationRequested)
{ }
}
else
{
try
{
await watcher.WaitForOwnerAsync(_watchCts.Token);
}
catch (OperationCanceledException) when (!_watchCts.IsCancellationRequested)
{ }
}
owner = watcher.GetCurrentOwner();
OnOwnerChanged(owner);
}
}
catch (Exception e)
catch (Exception e) when (!_isDisposed)
{
_serviceWatchDisposable = null;
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this, "Interface 'org.kde.StatusNotifierWatcher' is unavailable.\n{Exception}", e);
}
}
private void OnNameChange(string name, string? newOwner)
private void OnOwnerChanged(string? newOwner)
{
if (_isDisposed || _connection is null || name != "org.kde.StatusNotifierWatcher")
if (_isDisposed || _connection is null)
return;
if (!_serviceConnected && newOwner is not null)
{
_serviceConnected = true;
_statusNotifierWatcher = new OrgKdeStatusNotifierWatcherProxy(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
_statusNotifierWatcher = new StatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
DestroyTrayIcon();
@ -114,15 +134,11 @@ 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);
_connection.RemoveMethodHandler(_statusNotifierItemDbusObj!.Path);
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
_sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}");
await _dBus!.RequestNameAsync(_sysTrayServiceName, 0);
await _connection.RequestNameAsync(_sysTrayServiceName);
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
_statusNotifierItemDbusObj!.SetTitleAndTooltip(_tooltipText);
@ -134,9 +150,8 @@ 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);
_connection.RemoveMethodHandler(_statusNotifierItemDbusObj.Path);
}
public void Dispose()
@ -144,7 +159,7 @@ namespace Avalonia.FreeDesktop
IsActive = false;
DestroyTrayIcon();
(MenuExporter as IDisposable)?.Dispose();
_serviceWatchDisposable?.Dispose();
_watchCts?.Cancel();
_isDisposed = true;
}
@ -221,43 +236,72 @@ 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 : DBusHandler, IStatusNotifierItemHandler, IStatusNotifierItemProperties
{
public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath)
// The item is active, is more important that the item will be shown in some way to the user.
private const string StatusActive = "Active";
private string _category = "";
private string _id = "";
private string _title = "";
private ObjectPath _menu;
private (int, int, byte[])[] _iconPixmap = [];
public StatusNotifierItemDbusObj(DBusConnection connection, ObjectPath dbusMenuPath)
: base(connection, "/StatusNotifierItem", handlesChildPaths: false)
{
Connection = connection;
Menu = dbusMenuPath;
_menu = 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)
string IStatusNotifierItemProperties.Category => _category;
string IStatusNotifierItemProperties.Id => _id;
string IStatusNotifierItemProperties.Title => _title;
string IStatusNotifierItemProperties.Status => StatusActive;
int IStatusNotifierItemProperties.WindowId => 0;
string IStatusNotifierItemProperties.IconThemePath => "";
ObjectPath IStatusNotifierItemProperties.Menu => _menu;
bool IStatusNotifierItemProperties.ItemIsMenu => false;
string IStatusNotifierItemProperties.IconName => "";
(int, int, byte[])[] IStatusNotifierItemProperties.IconPixmap => _iconPixmap;
string IStatusNotifierItemProperties.OverlayIconName => "";
(int, int, byte[])[] IStatusNotifierItemProperties.OverlayIconPixmap => [];
string IStatusNotifierItemProperties.AttentionIconName => "";
(int, int, byte[])[] IStatusNotifierItemProperties.AttentionIconPixmap => [];
string IStatusNotifierItemProperties.AttentionMovieName => "";
(string, (int, int, byte[])[], string, string) IStatusNotifierItemProperties.ToolTip => ("", [], "", "");
ValueTask IStatusNotifierItemHandler.HandleGetPropertyAsync(IStatusNotifierItemHandler.GetPropertyContext context)
=> context.Handle(this);
ValueTask IStatusNotifierItemHandler.HandleGetAllPropertiesAsync(IStatusNotifierItemHandler.GetAllPropertiesContext context)
=> context.Handle(this);
ValueTask IStatusNotifierItemHandler.ContextMenuAsync(int x, int y) => new();
ValueTask IStatusNotifierItemHandler.ActivateAsync(int x, int y)
{
ActivationDelegate?.Invoke();
return new ValueTask();
}
protected override ValueTask OnSecondaryActivateAsync(Message message, int x, int y) => new();
ValueTask IStatusNotifierItemHandler.SecondaryActivateAsync(int x, int y) => new();
protected override ValueTask OnScrollAsync(Message message, int delta, string orientation) => new();
ValueTask IStatusNotifierItemHandler.ScrollAsync(int delta, string orientation) => new();
public void InvalidateAll()
{
EmitNewTitle();
EmitNewIcon();
EmitNewAttentionIcon();
EmitNewOverlayIcon();
EmitNewToolTip();
EmitNewStatus(Status);
Connection.EmitNewTitle(Path);
Connection.EmitNewIcon(Path);
Connection.EmitNewAttentionIcon(Path);
Connection.EmitNewOverlayIcon(Path);
Connection.EmitNewToolTip(Path);
Connection.EmitNewStatus(Path, StatusActive);
}
public void SetIcon((int, int, byte[]) dbusPixmap)
{
IconPixmap = [dbusPixmap];
_iconPixmap = [dbusPixmap];
InvalidateAll();
}
@ -266,10 +310,9 @@ namespace Avalonia.FreeDesktop
if (text is null)
return;
Id = text;
Category = "ApplicationStatus";
Status = text;
Title = text;
_id = text;
_category = "ApplicationStatus";
_title = text;
InvalidateAll();
}
}

Loading…
Cancel
Save