Browse Source

Bump DBus stack take 2 (#15685)

* Bump DBus stack
- Use new Variant system

* Fix merge issues

* Intentionally break the DBus spec

* Dispose DBus connection and signal watchers

* Bump Tmds,DBus.Protocol

* Dispose DBus objects correctly

* Use PathHandler for DBus objects

* Revert to old initialization

* Bump DBus packages

* Fix global menu

* Add comment about wrapped variants

* Bump Tmds.DBus.SourceGenerator

* Update api baseline

* Bump Tmds.DBus stack

---------

Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/16705/head
affederaffe 2 years ago
committed by GitHub
parent
commit
1d9a0183eb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      api/Avalonia.FreeDesktop.nupkg.xml
  2. 4
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  3. 4
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  4. 2
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  5. 21
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  6. 130
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  7. 42
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  8. 109
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  9. 56
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  10. 3
      src/Avalonia.X11/X11Window.cs

10
api/Avalonia.FreeDesktop.nupkg.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Tmds.DBus.SourceGenerator.PropertyChanges`1</Target>
<Left>baseline/netstandard2.0/Avalonia.FreeDesktop.dll</Left>
<Right>target/netstandard2.0/Avalonia.FreeDesktop.dll</Right>
</Suppression>
</Suppressions>

4
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -12,8 +12,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.16.0" /> <PackageReference Include="Tmds.DBus.Protocol" Version="0.20.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.15" PrivateAssets="all" /> <PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.19" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

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

@ -45,7 +45,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
public ValueTask<IDisposable> WatchForwardKeyAsync(Action<Exception?, (uint keyval, uint state, int type)> handler) => public ValueTask<IDisposable> WatchForwardKeyAsync(Action<Exception?, (uint keyval, uint state, int type)> handler) =>
_old?.WatchForwardKeyAsync(handler) _old?.WatchForwardKeyAsync(handler)
?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0))) ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0)))
?? new ValueTask<IDisposable>(Disposable.Empty); ?? new ValueTask<IDisposable>(Disposable.Empty);
public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync( public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync(
@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
_old?.WatchUpdateFormattedPreeditAsync(handler!) _old?.WatchUpdateFormattedPreeditAsync(handler!)
?? _modern?.WatchUpdateFormattedPreeditAsync(handler!) ?? _modern?.WatchUpdateFormattedPreeditAsync(handler!)
?? new ValueTask<IDisposable>(Disposable.Empty); ?? new ValueTask<IDisposable>(Disposable.Empty);
public Task SetCapacityAsync(uint flags) => public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;
} }

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

@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
var resp = await method.CreateICv3Async(GetAppName(), var resp = await method.CreateICv3Async(GetAppName(),
Process.GetCurrentProcess().Id); Process.GetCurrentProcess().Id);
var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.icid}"); var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.Icid}");
_context = new FcitxICWrapper(proxy); _context = new FcitxICWrapper(proxy);
} }
else else

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

@ -51,17 +51,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
Client.SetPreeditText(_preeditText, _preeditText == null ? null : _preeditCursor); Client.SetPreeditText(_preeditText, _preeditText == null ? null : _preeditCursor);
} }
private void OnUpdatePreedit(Exception? arg1, (DBusVariantItem text, uint cursor_pos, bool visible) preeditComponents) private void OnUpdatePreedit(Exception? arg1, (VariantValue Text, uint CursorPos, bool Visible) preeditComponents)
{ {
if (preeditComponents.text is { Value: DBusStructItem { Count: >= 3 } structItem } && if (preeditComponents.Text is { Type: VariantValueType.Struct, Count: >= 3 } structItem && structItem.GetItem(2) is { Type: VariantValueType.String} stringItem)
structItem[2] is DBusStringItem stringItem)
{ {
_preeditText = stringItem.Value; _preeditText = stringItem.GetString();
_preeditCursor = _preeditText != null _preeditCursor = _preeditText != null
? Utf16Utils.CharacterOffsetToStringOffset(_preeditText, ? Utf16Utils.CharacterOffsetToStringOffset(_preeditText,
(int)Math.Min(preeditComponents.cursor_pos, int.MaxValue), false) (int)Math.Min(preeditComponents.CursorPos, int.MaxValue), false)
: 0; : 0;
_preeditShown = true; _preeditShown = true;
} }
else else
@ -102,13 +101,13 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
}); });
} }
private void OnCommitText(Exception? e, DBusVariantItem variantItem) private void OnCommitText(Exception? e, VariantValue variantItem)
{ {
if (_insideReset > 0) if (_insideReset > 0)
{ {
// For some reason iBus can trigger a CommitText while being reset. // For some reason iBus can trigger a CommitText while being reset.
// Thankfully the signal is sent _during_ Reset call processing, // Thankfully the signal is sent _during_ Reset call processing,
// so it arrives on-the-wire before Reset call result, so we can // so it arrives on-the-wire before Reset call result, so we can
// check if we have any pending Reset calls and ignore the signal here // check if we have any pending Reset calls and ignore the signal here
return; return;
} }
@ -118,8 +117,8 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
return; return;
} }
if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem) if (variantItem.Count >= 3 && variantItem.GetItem(2) is { Type: VariantValueType.String } stringItem)
FireCommit(stringItem.Value); FireCommit(stringItem.GetString());
} }
protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask;

130
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@ -24,11 +24,12 @@ namespace Avalonia.FreeDesktop
public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}";
private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable private sealed class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable
{ {
private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new(); private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new(); private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new();
private readonly HashSet<NativeMenu> _menus = new(); private readonly HashSet<NativeMenu> _menus = [];
private readonly PathHandler _pathHandler;
private readonly uint _xid; private readonly uint _xid;
private readonly bool _appMenu = true; private readonly bool _appMenu = true;
private ComCanonicalAppMenuRegistrar? _registrar; private ComCanonicalAppMenuRegistrar? _registrar;
@ -40,37 +41,29 @@ namespace Avalonia.FreeDesktop
public DBusMenuExporterImpl(Connection connection, IntPtr xid) public DBusMenuExporterImpl(Connection connection, IntPtr xid)
{ {
InitBackingProperties(); Version = 4;
Connection = connection; Connection = connection;
_xid = (uint)xid.ToInt32(); _xid = (uint)xid.ToInt32();
Path = GenerateDBusMenuObjPath; _pathHandler = new PathHandler(GenerateDBusMenuObjPath);
SetNativeMenu(new NativeMenu()); _pathHandler.Add(this);
SetNativeMenu([]);
_ = InitializeAsync(); _ = InitializeAsync();
} }
public DBusMenuExporterImpl(Connection connection, string path) public DBusMenuExporterImpl(Connection connection, string path)
{ {
InitBackingProperties(); Version = 4;
Connection = connection; Connection = connection;
_appMenu = false; _appMenu = false;
Path = path; _pathHandler = new PathHandler(path);
SetNativeMenu(new NativeMenu()); _pathHandler.Add(this);
SetNativeMenu([]);
_ = InitializeAsync(); _ = InitializeAsync();
} }
private void InitBackingProperties() public override Connection Connection { get; }
{
BackingProperties.Version = 4;
BackingProperties.Status = string.Empty;
BackingProperties.TextDirection = string.Empty;
BackingProperties.IconThemePath = Array.Empty<string>();
}
protected override Connection Connection { get; }
public override string Path { get; } protected override ValueTask<(uint Revision, (int, Dictionary<string, Variant>, Variant[]) Layout)> OnGetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames)
protected override ValueTask<(uint revision, (int, Dictionary<string, DBusVariantItem>, DBusVariantItem[]) layout)> OnGetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames)
{ {
var menu = GetMenu(parentId); var menu = GetMenu(parentId);
var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames);
@ -80,36 +73,36 @@ namespace Avalonia.FreeDesktop
OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty);
} }
return new ValueTask<(uint, (int, Dictionary<string, DBusVariantItem>, DBusVariantItem[]))>((_revision, layout)); return new ValueTask<(uint, (int, Dictionary<string, Variant>, Variant[]))>((_revision, layout));
} }
protected override ValueTask<(int, Dictionary<string, DBusVariantItem>)[]> OnGetGroupPropertiesAsync(int[] ids, string[] propertyNames) protected override ValueTask<(int, Dictionary<string, Variant>)[]> OnGetGroupPropertiesAsync(int[] ids, string[] propertyNames)
=> new(ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray()); => new(ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray());
protected override ValueTask<DBusVariantItem> OnGetPropertyAsync(int id, string name) => protected override ValueTask<Variant> OnGetPropertyAsync(int id, string name) =>
new(GetProperty(GetMenu(id), name) ?? new DBusVariantItem("i", new DBusInt32Item(0))); new(GetProperty(GetMenu(id), name) ?? new Variant(0));
protected override ValueTask OnEventAsync(int id, string eventId, DBusVariantItem data, uint timestamp) protected override ValueTask OnEventAsync(int id, string eventId, VariantValue data, uint timestamp)
{ {
HandleEvent(id, eventId); HandleEvent(id, eventId);
return new ValueTask(); return new ValueTask();
} }
protected override ValueTask<int[]> OnEventGroupAsync((int, string, DBusVariantItem, uint)[] events) protected override ValueTask<int[]> OnEventGroupAsync((int, string, VariantValue, uint)[] events)
{ {
foreach (var e in events) foreach (var e in events)
HandleEvent(e.Item1, e.Item2); HandleEvent(e.Item1, e.Item2);
return new ValueTask<int[]>(Array.Empty<int>()); return new ValueTask<int[]>([]);
} }
protected override ValueTask<bool> OnAboutToShowAsync(int id) => new(false); protected override ValueTask<bool> OnAboutToShowAsync(int id) => new(false);
protected override ValueTask<(int[] updatesNeeded, int[] idErrors)> OnAboutToShowGroupAsync(int[] ids) => protected override ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> OnAboutToShowGroupAsync(int[] ids) =>
new((Array.Empty<int>(), Array.Empty<int>())); new(([], []));
private async Task InitializeAsync() private async Task InitializeAsync()
{ {
Connection.AddMethodHandler(this); Connection.AddMethodHandler(_pathHandler);
if (!_appMenu) if (!_appMenu)
return; return;
@ -117,7 +110,7 @@ namespace Avalonia.FreeDesktop
try try
{ {
if (!_disposed) if (!_disposed)
await _registrar.RegisterWindowAsync(_xid, Path); await _registrar.RegisterWindowAsync(_xid, _pathHandler.Path);
} }
catch catch
{ {
@ -136,17 +129,17 @@ namespace Avalonia.FreeDesktop
_disposed = true; _disposed = true;
// Fire and forget // Fire and forget
_ = _registrar?.UnregisterWindowAsync(_xid); _ = _registrar?.UnregisterWindowAsync(_xid);
_pathHandler.Remove(this);
Connection.RemoveMethodHandler(_pathHandler.Path);
} }
public bool IsNativeMenuExported { get; private set; } public bool IsNativeMenuExported { get; private set; }
public event EventHandler? OnIsNativeMenuExportedChanged; public event EventHandler? OnIsNativeMenuExportedChanged;
public void SetNativeMenu(NativeMenu? menu) public void SetNativeMenu(NativeMenu? menu)
{ {
menu ??= new NativeMenu(); menu ??= [];
if (_menu is not null) if (_menu is not null)
((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
@ -216,39 +209,34 @@ namespace Avalonia.FreeDesktop
private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) => QueueReset(); private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) => QueueReset();
private static readonly string[] s_allProperties = { private static readonly string[] s_allProperties = ["type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"];
"type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
};
private static DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) private static Variant? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name)
{ {
var (it, menu) = i; var (it, menu) = i;
if (it is NativeMenuItemSeparator) if (it is NativeMenuItemSeparator)
{ {
if (name == "type") if (name == "type")
return new DBusVariantItem("s", new DBusStringItem("separator")); return new Variant("separator");
} }
else if (it is NativeMenuItem item) else if (it is NativeMenuItem item)
{ {
if (name == "type") if (name == "type")
return null; return null;
if (name == "label") if (name == "label")
return new DBusVariantItem("s", new DBusStringItem(item.Header ?? "<null>")); return new Variant(item.Header ?? "<null>");
if (name == "enabled") if (name == "enabled")
{ {
if (item.Menu is not null && item.Menu.Items.Count == 0) if (item.Menu is not null && item.Menu.Items.Count == 0)
return new DBusVariantItem("b", new DBusBoolItem(false)); return new Variant(false);
if (!item.IsEnabled) if (!item.IsEnabled)
return new DBusVariantItem("b", new DBusBoolItem(false)); return new Variant(false);
return null; return null;
} }
if (name == "visible") { if (name == "visible")
if (!item.IsVisible) return new Variant(item.IsVisible);
return new DBusVariantItem("b", new DBusBoolItem(false));
return new DBusVariantItem("b", new DBusBoolItem(true));
}
if (name == "shortcut") if (name == "shortcut")
{ {
@ -256,30 +244,30 @@ namespace Avalonia.FreeDesktop
return null; return null;
if (item.Gesture.KeyModifiers == 0) if (item.Gesture.KeyModifiers == 0)
return null; return null;
var lst = new List<DBusItem>(); var lst = new Array<string>();
var mod = item.Gesture; var mod = item.Gesture;
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control))
lst.Add(new DBusStringItem("Control")); lst.Add("Control");
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt))
lst.Add(new DBusStringItem("Alt")); lst.Add("Alt");
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift))
lst.Add(new DBusStringItem("Shift")); lst.Add("Shift");
if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta))
lst.Add(new DBusStringItem("Super")); lst.Add("Super");
lst.Add(new DBusStringItem(item.Gesture.Key.ToString())); lst.Add(item.Gesture.Key.ToString());
return new DBusVariantItem("aas", new DBusArrayItem(DBusType.Array, new[] { new DBusArrayItem(DBusType.String, lst) })); return Variant.FromArray(new Array<Array<string>>([lst]));
} }
if (name == "toggle-type") if (name == "toggle-type")
{ {
if (item.ToggleType == NativeMenuItemToggleType.CheckBox) if (item.ToggleType == NativeMenuItemToggleType.CheckBox)
return new DBusVariantItem("s", new DBusStringItem("checkmark")); return new Variant("checkmark");
if (item.ToggleType == NativeMenuItemToggleType.Radio) if (item.ToggleType == NativeMenuItemToggleType.Radio)
return new DBusVariantItem("s", new DBusStringItem("radio")); return new Variant("radio");
} }
if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None)
return new DBusVariantItem("i", new DBusInt32Item(item.IsChecked ? 1 : 0)); return new Variant(item.IsChecked ? 1 : 0);
if (name == "icon-data") if (name == "icon-data")
{ {
@ -292,57 +280,55 @@ namespace Avalonia.FreeDesktop
var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item);
using var ms = new MemoryStream(); using var ms = new MemoryStream();
icon.Save(ms); icon.Save(ms);
return new DBusVariantItem("ay", new DBusByteArrayItem(ms.ToArray())); return Variant.FromArray(new Array<byte>(ms.ToArray()));
} }
} }
} }
if (name == "children-display") if (name == "children-display")
return menu is not null ? new DBusVariantItem("s", new DBusStringItem("submenu")) : null; {
if (menu is not null)
return new Variant("submenu");
return null;
}
} }
return null; return null;
} }
private static Dictionary<string, DBusVariantItem> GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) private static Dictionary<string, Variant> GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
{ {
if (names.Length == 0) if (names.Length == 0)
names = s_allProperties; names = s_allProperties;
var properties = new Dictionary<string, DBusVariantItem>(); var properties = new Dictionary<string, Variant>();
foreach (var n in names) foreach (var n in names)
{ {
var v = GetProperty(i, n); var v = GetProperty(i, n);
if (v is not null) if (v.HasValue)
properties.Add(n, v); properties.Add(n, v.Value);
} }
return properties; return properties;
} }
private (int, Dictionary<string, DBusVariantItem>, DBusVariantItem[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) private (int, Dictionary<string, Variant>, Variant[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames)
{ {
var id = item is null ? 0 : GetId(item); var id = item is null ? 0 : GetId(item);
var props = GetProperties((item, menu), propertyNames); var props = GetProperties((item, menu), propertyNames);
var children = depth == 0 || menu is null ? Array.Empty<DBusVariantItem>() : new DBusVariantItem[menu.Items.Count]; var children = depth == 0 || menu is null ? [] : new Variant[menu.Items.Count];
if (menu is not null) if (menu is not null)
{ {
for (var c = 0; c < children.Length; c++) for (var c = 0; c < children.Length; c++)
{ {
var ch = menu.Items[c]; var ch = menu.Items[c];
var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames);
children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[] children[c] = Variant.FromStruct(Struct.Create(layout.Item1, new Dict<string, Variant>(layout.Item2), new Array<Variant>(layout.Item3)));
{
new DBusInt32Item(layout.Item1),
new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value)).ToArray()),
new DBusArrayItem(DBusType.Variant, layout.Item3)
}));
} }
} }
return (id, props, children); return (id, props, children);
} }
private void HandleEvent(int id, string eventId) private void HandleEvent(int id, string eventId)
{ {
if (eventId == "clicked") if (eventId == "clicked")

42
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@ -18,9 +18,8 @@ namespace Avalonia.FreeDesktop
public DBusPlatformSettings() public DBusPlatformSettings()
{ {
if (DBusHelper.DefaultConnection is not {} conn) if (DBusHelper.DefaultConnection is not { } conn)
return; return;
using var restoreContext = AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Input);
_settings = new OrgFreedesktopPortalSettings(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); _settings = new OrgFreedesktopPortalSettings(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
@ -35,7 +34,7 @@ namespace Avalonia.FreeDesktop
_accentColor = await TryGetAccentColorAsync(); _accentColor = await TryGetAccentColorAsync();
_lastColorValues = BuildPlatformColorValues(); _lastColorValues = BuildPlatformColorValues();
if (_lastColorValues is not null) if (_lastColorValues is not null)
Threading.Dispatcher.UIThread.Post(() => OnColorValuesChanged(_lastColorValues)); Dispatcher.UIThread.Post(() => OnColorValuesChanged(_lastColorValues));
} }
private async Task<PlatformThemeVariant?> TryGetThemeVariantAsync() private async Task<PlatformThemeVariant?> TryGetThemeVariantAsync()
@ -43,14 +42,13 @@ namespace Avalonia.FreeDesktop
try try
{ {
var version = await _settings!.GetVersionPropertyAsync(); var version = await _settings!.GetVersionPropertyAsync();
DBusVariantItem value; VariantValue value;
if (version >= 2) if (version >= 2)
value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "color-scheme"); value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "color-scheme");
else else
value = (DBusVariantItem)(await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme")).Value; // Variants-in-Variants are automatically collapsed by Tmds.DBus.Protocol, so need to do so here as normally necessary
if (value.Value is DBusUInt32Item dBusUInt32Item) value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
return ToColorScheme(dBusUInt32Item.Value); return ToColorScheme(value.GetUInt32());
return null;
} }
catch (DBusException) catch (DBusException)
{ {
@ -63,14 +61,12 @@ namespace Avalonia.FreeDesktop
try try
{ {
var version = await _settings!.GetVersionPropertyAsync(); var version = await _settings!.GetVersionPropertyAsync();
DBusVariantItem value; VariantValue value;
if (version >= 2) if (version >= 2)
value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "accent-color"); value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "accent-color");
else else
value = (DBusVariantItem)(await _settings!.ReadAsync("org.freedesktop.appearance", "accent-color")).Value; value = await _settings!.ReadAsync("org.freedesktop.appearance", "accent-color");
if (value.Value is DBusStructItem dBusStructItem) return ToAccentColor(value);
return ToAccentColor(dBusStructItem);
return null;
} }
catch (DBusException) catch (DBusException)
{ {
@ -78,20 +74,20 @@ namespace Avalonia.FreeDesktop
} }
} }
private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) private void SettingsChangedHandler(Exception? exception, (string Namespace, string Key, VariantValue Value) tuple)
{ {
if (exception is not null) if (exception is not null)
return; return;
switch (valueTuple) switch (tuple)
{ {
case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): case ("org.freedesktop.appearance", "color-scheme", var colorScheme):
_themeVariant = ToColorScheme((colorScheme.Value as DBusUInt32Item)!.Value); _themeVariant = ToColorScheme(colorScheme.GetUInt32());
_lastColorValues = BuildPlatformColorValues(); _lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!); OnColorValuesChanged(_lastColorValues!);
break; break;
case ("org.freedesktop.appearance", "accent-color", { } accentColor): case ("org.freedesktop.appearance", "accent-color", var accentColor):
_accentColor = ToAccentColor((accentColor.Value as DBusStructItem)!); _accentColor = ToAccentColor(accentColor);
_lastColorValues = BuildPlatformColorValues(); _lastColorValues = BuildPlatformColorValues();
OnColorValuesChanged(_lastColorValues!); OnColorValuesChanged(_lastColorValues!);
break; break;
@ -120,16 +116,16 @@ namespace Avalonia.FreeDesktop
return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light;
} }
private static Color? ToAccentColor(DBusStructItem value) private static Color? ToAccentColor(VariantValue value)
{ {
/* /*
Indicates the system's preferred accent color as a tuple of RGB values Indicates the system's preferred accent color as a tuple of RGB values
in the sRGB color space, in the range [0,1]. in the sRGB color space, in the range [0,1].
Out-of-range RGB values should be treated as an unset accent color. Out-of-range RGB values should be treated as an unset accent color.
*/ */
var r = (value[0] as DBusDoubleItem)!.Value; var r = value.GetItem(0).GetDouble();
var g = (value[1] as DBusDoubleItem)!.Value; var g = value.GetItem(1).GetDouble();
var b = (value[2] as DBusDoubleItem)!.Value; var b = value.GetItem(2).GetDouble();
if (r is < 0 or > 1 || g is < 0 or > 1 || b is < 0 or > 1) if (r is < 0 or > 1 || g is < 0 or > 1 || b is < 0 or > 1)
return null; return null;
return Color.FromRgb((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); return Color.FromRgb((byte)(r * 255), (byte)(g * 255), (byte)(b * 255));

109
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -59,14 +59,15 @@ namespace Avalonia.FreeDesktop
{ {
var parentWindow = $"x11:{_handle.Handle:X}"; var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath; ObjectPath objectPath;
var chooserOptions = new Dictionary<string, DBusVariantItem>(); var chooserOptions = new Dictionary<string, Variant>();
var filters = ParseFilters(options.FileTypeFilter);
if (filters is not null) if (TryParseFilters(options.FileTypeFilter, out var filters))
chooserOptions.Add("filters", filters); chooserOptions.Add("filters", filters);
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0")))); chooserOptions.Add("current_folder", Variant.FromArray(new Array<byte>(Encoding.UTF8.GetBytes(folderPath + "\0"))));
chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)));
chooserOptions.Add("multiple", new Variant(options.AllowMultiple));
objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
@ -77,7 +78,7 @@ namespace Avalonia.FreeDesktop
if (e is not null) if (e is not null)
tsc.TrySetException(e); tsc.TrySetException(e);
else else
tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); tsc.TrySetResult(x.Results["uris"].GetArray<string>());
}); });
var uris = await tsc.Task ?? Array.Empty<string>(); var uris = await tsc.Task ?? Array.Empty<string>();
@ -88,15 +89,14 @@ namespace Avalonia.FreeDesktop
{ {
var parentWindow = $"x11:{_handle.Handle:X}"; var parentWindow = $"x11:{_handle.Handle:X}";
ObjectPath objectPath; ObjectPath objectPath;
var chooserOptions = new Dictionary<string, DBusVariantItem>(); var chooserOptions = new Dictionary<string, Variant>();
var filters = ParseFilters(options.FileTypeChoices); if (TryParseFilters(options.FileTypeChoices, out var filters))
if (filters is not null)
chooserOptions.Add("filters", filters); chooserOptions.Add("filters", filters);
if (options.SuggestedFileName is { } currentName) if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); chooserOptions.Add("current_name", new Variant(currentName));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0")))); chooserOptions.Add("current_folder", Variant.FromArray(new Array<byte>(Encoding.UTF8.GetBytes(folderPath + "\0"))));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
@ -105,41 +105,31 @@ namespace Avalonia.FreeDesktop
using var disposable = await request.WatchResponseAsync((e, x) => using var disposable = await request.WatchResponseAsync((e, x) =>
{ {
if (e is not null) if (e is not null)
{
tsc.TrySetException(e); tsc.TrySetException(e);
}
else else
{ {
if(x.results.TryGetValue("current_filter", out var value)) if (x.Results.TryGetValue("current_filter", out var currentFilter))
{ {
var currentFilter = value.Value as DBusStructItem; var name = currentFilter.GetItem(0).GetString();
if(currentFilter != null) selectedType = new FilePickerFileType(name);
var patterns = new List<string>();
var mimeTypes = new List<string>();
var types = currentFilter.GetItem(1).GetArray<VariantValue>();
foreach(var t in types)
{ {
var name = (currentFilter[0] as DBusStringItem)?.Value.ToString() ?? ""; if (t.GetItem(0).GetUInt32() == 1)
selectedType = new FilePickerFileType(name); mimeTypes.Add(t.GetItem(1).GetString());
if(currentFilter[1] is DBusArrayItem types) else
{ patterns.Add(t.GetItem(1).GetString());
List<string> filters = new List<string>();
List<string> mimeTypes = new List<string>();
foreach(var t in types)
{
if(t is DBusStructItem filter)
{
if((filter[0] as DBusUInt32Item)?.Value == 1)
{
mimeTypes.Add((filter[1] as DBusStringItem)?.Value.ToString() ?? "");
}
else
{
filters.Add((filter[1] as DBusStringItem)?.Value.ToString() ?? "");
}
}
}
selectedType.Patterns = filters;
selectedType.MimeTypes = mimeTypes;
}
} }
selectedType.Patterns = patterns;
selectedType.MimeTypes = mimeTypes;
} }
tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray());
tsc.TrySetResult(x.Results["uris"].GetArray<string>());
} }
}); });
@ -160,16 +150,16 @@ namespace Avalonia.FreeDesktop
return Array.Empty<IStorageFolder>(); return Array.Empty<IStorageFolder>();
var parentWindow = $"x11:{_handle.Handle:X}"; var parentWindow = $"x11:{_handle.Handle:X}";
var chooserOptions = new Dictionary<string, DBusVariantItem> var chooserOptions = new Dictionary<string, Variant>
{ {
{ "directory", new DBusVariantItem("b", new DBusBoolItem(true)) }, { "directory", new Variant(true) },
{ "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) } { "multiple", new Variant(options.AllowMultiple) }
}; };
if (options.SuggestedFileName is { } currentName) if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); chooserOptions.Add("current_name", new Variant(currentName));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0")))); chooserOptions.Add("current_folder", Variant.FromArray(new Array<byte>(Encoding.UTF8.GetBytes(folderPath + "\0"))));
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
@ -179,7 +169,7 @@ namespace Avalonia.FreeDesktop
if (e is not null) if (e is not null)
tsc.TrySetException(e); tsc.TrySetException(e);
else else
tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); tsc.TrySetResult(x.Results["uris"].GetArray<string>());
}); });
var uris = await tsc.Task ?? Array.Empty<string>(); var uris = await tsc.Task ?? Array.Empty<string>();
@ -190,40 +180,35 @@ namespace Avalonia.FreeDesktop
.Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList();
} }
private static DBusVariantItem? ParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes) private static bool TryParseFilters(IReadOnlyList<FilePickerFileType>? fileTypes, out Variant result)
{ {
const uint GlobStyle = 0u; const uint GlobStyle = 0u;
const uint MimeStyle = 1u; const uint MimeStyle = 1u;
// Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
if (fileTypes is null) if (fileTypes is null)
return null; {
result = default;
return false;
}
var filters = new List<DBusItem>(); var filters = new Array<Struct<string, Array<Struct<uint, string>>>>();
foreach (var fileType in fileTypes) foreach (var fileType in fileTypes)
{ {
var extensions = new List<DBusItem>(); var extensions = new List<Struct<uint, string>>();
if (fileType.Patterns?.Count > 0) if (fileType.Patterns?.Count > 0)
extensions.AddRange( extensions.AddRange(fileType.Patterns.Select(static pattern => Struct.Create(GlobStyle, pattern)));
fileType.Patterns.Select(static pattern =>
new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) })));
else if (fileType.MimeTypes?.Count > 0) else if (fileType.MimeTypes?.Count > 0)
extensions.AddRange( extensions.AddRange(fileType.MimeTypes.Select(static mimeType => Struct.Create(MimeStyle, mimeType)));
fileType.MimeTypes.Select(static mimeType =>
new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) })));
else else
continue; continue;
filters.Add(new DBusStructItem( filters.Add(Struct.Create(fileType.Name, new Array<Struct<uint, string>>(extensions)));
new DBusItem[]
{
new DBusStringItem(fileType.Name),
new DBusArrayItem(DBusType.Struct, extensions)
}));
} }
return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", new DBusArrayItem(DBusType.Struct, filters)) : null; result = Variant.FromArray(filters);
return true;
} }
} }
} }

56
src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Logging; using Avalonia.Logging;
@ -15,13 +13,13 @@ namespace Avalonia.FreeDesktop
internal class DBusTrayIconImpl : ITrayIconImpl internal class DBusTrayIconImpl : ITrayIconImpl
{ {
private static int s_trayIconInstanceId; private static int s_trayIconInstanceId;
public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, [255, 0, 0, 0]);
private readonly ObjectPath _dbusMenuPath;
private readonly Connection? _connection; private readonly Connection? _connection;
private readonly OrgFreedesktopDBus? _dBus; private readonly OrgFreedesktopDBus? _dBus;
private IDisposable? _serviceWatchDisposable; private IDisposable? _serviceWatchDisposable;
private readonly PathHandler _pathHandler = new("/StatusNotifierItem");
private readonly StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher; private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher;
private (int, int, byte[]) _icon; private (int, int, byte[]) _icon;
@ -53,13 +51,14 @@ namespace Avalonia.FreeDesktop
IsActive = true; IsActive = true;
_dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; var dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusMenuPath, _connection);
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, dbusMenuPath);
_pathHandler.Add(_statusNotifierItemDbusObj);
_connection.AddMethodHandler(_pathHandler);
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath);
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
WatchAsync(); WatchAsync();
} }
@ -84,7 +83,7 @@ namespace Avalonia.FreeDesktop
if (_isDisposed || _connection is null || name != "org.kde.StatusNotifierWatcher") if (_isDisposed || _connection is null || name != "org.kde.StatusNotifierWatcher")
return; return;
if (!_serviceConnected & newOwner is not null) if (!_serviceConnected && newOwner is not null)
{ {
_serviceConnected = true; _serviceConnected = true;
_statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
@ -128,14 +127,17 @@ namespace Avalonia.FreeDesktop
return; return;
_dBus!.ReleaseNameAsync(_sysTrayServiceName); _dBus!.ReleaseNameAsync(_sysTrayServiceName);
_pathHandler.Remove(_statusNotifierItemDbusObj);
_connection.RemoveMethodHandler(_pathHandler.Path);
} }
public void Dispose() public void Dispose()
{ {
IsActive = false; IsActive = false;
_isDisposed = true;
DestroyTrayIcon(); DestroyTrayIcon();
(MenuExporter as IDisposable)?.Dispose();
_serviceWatchDisposable?.Dispose(); _serviceWatchDisposable?.Dispose();
_isDisposed = true;
} }
public void SetIcon(IWindowIconImpl? icon) public void SetIcon(IWindowIconImpl? icon)
@ -216,24 +218,10 @@ namespace Avalonia.FreeDesktop
public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath)
{ {
Connection = connection; Connection = connection;
BackingProperties.Menu = dbusMenuPath; Menu = dbusMenuPath;
BackingProperties.Category = string.Empty;
BackingProperties.Status = string.Empty;
BackingProperties.Id = string.Empty;
BackingProperties.Title = string.Empty;
BackingProperties.IconPixmap = Array.Empty<(int, int, byte[])>();
BackingProperties.AttentionIconName = string.Empty;
BackingProperties.AttentionIconPixmap = Array.Empty<(int, int, byte[])>();
BackingProperties.AttentionMovieName = string.Empty;
BackingProperties.OverlayIconName = string.Empty;
BackingProperties.OverlayIconPixmap = Array.Empty<(int, int, byte[])>();
BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty);
InvalidateAll();
} }
protected override Connection Connection { get; } public override Connection Connection { get; }
public override string Path => "/StatusNotifierItem";
public event Action? ActivationDelegate; public event Action? ActivationDelegate;
@ -256,12 +244,12 @@ namespace Avalonia.FreeDesktop
EmitNewAttentionIcon(); EmitNewAttentionIcon();
EmitNewOverlayIcon(); EmitNewOverlayIcon();
EmitNewToolTip(); EmitNewToolTip();
EmitNewStatus(BackingProperties.Status); EmitNewStatus(Status);
} }
public void SetIcon((int, int, byte[]) dbusPixmap) public void SetIcon((int, int, byte[]) dbusPixmap)
{ {
BackingProperties.IconPixmap = new[] { dbusPixmap }; IconPixmap = [dbusPixmap];
InvalidateAll(); InvalidateAll();
} }
@ -270,10 +258,10 @@ namespace Avalonia.FreeDesktop
if (text is null) if (text is null)
return; return;
BackingProperties.Id = text; Id = text;
BackingProperties.Category = "ApplicationStatus"; Category = "ApplicationStatus";
BackingProperties.Status = text; Status = text;
BackingProperties.Title = text; Title = text;
InvalidateAll(); InvalidateAll();
} }
} }

3
src/Avalonia.X11/X11Window.cs

@ -955,6 +955,9 @@ namespace Avalonia.X11
if (_handle != IntPtr.Zero) if (_handle != IntPtr.Zero)
Closed?.Invoke(); Closed?.Invoke();
if (_nativeMenuExporter is IDisposable disposable)
disposable.Dispose();
if (_rawEventGrouper != null) if (_rawEventGrouper != null)
{ {
_rawEventGrouper.Dispose(); _rawEventGrouper.Dispose();

Loading…
Cancel
Save