Răsfoiți Sursa

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 <[email protected]>
affederaffe 1 an în urmă
părinte
comite
1d9a0183eb

+ 10 - 0
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>

+ 2 - 2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

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

+ 2 - 2
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) =>
             _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);
 
         public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync(
@@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
             _old?.WatchUpdateFormattedPreeditAsync(handler!)
             ?? _modern?.WatchUpdateFormattedPreeditAsync(handler!)
             ?? new ValueTask<IDisposable>(Disposable.Empty);
-        
+
         public Task SetCapacityAsync(uint flags) =>
             _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;
     }

+ 1 - 1
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs

@@ -29,7 +29,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx
                 var resp = await method.CreateICv3Async(GetAppName(),
                     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);
             }
             else

+ 10 - 11
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs

@@ -51,17 +51,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
                 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 } &&
-                structItem[2] is DBusStringItem stringItem)
+            if (preeditComponents.Text is { Type: VariantValueType.Struct, Count: >= 3 } structItem && structItem.GetItem(2) is { Type: VariantValueType.String} stringItem)
             {
-                _preeditText = stringItem.Value;
+                _preeditText = stringItem.GetString();
                 _preeditCursor = _preeditText != null
                     ? Utf16Utils.CharacterOffsetToStringOffset(_preeditText,
-                        (int)Math.Min(preeditComponents.cursor_pos, int.MaxValue), false)
+                        (int)Math.Min(preeditComponents.CursorPos, int.MaxValue), false)
                     : 0;
-                
+
                 _preeditShown = true;
             }
             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.
                 // 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
                 return;
             }
@@ -118,8 +117,8 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus
                 return;
             }
 
-            if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem)
-                FireCommit(stringItem.Value);
+            if (variantItem.Count >= 3 && variantItem.GetItem(2) is { Type: VariantValueType.String } stringItem)
+                FireCommit(stringItem.GetString());
         }
 
         protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask;

+ 58 - 72
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

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

+ 19 - 23
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

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

+ 47 - 62
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@@ -59,14 +59,15 @@ namespace Avalonia.FreeDesktop
         {
             var parentWindow = $"x11:{_handle.Handle:X}";
             ObjectPath objectPath;
-            var chooserOptions = new Dictionary<string, DBusVariantItem>();
-            var filters = ParseFilters(options.FileTypeFilter);
-            if (filters is not null)
+            var chooserOptions = new Dictionary<string, Variant>();
+
+            if (TryParseFilters(options.FileTypeFilter, out var filters))
                 chooserOptions.Add("filters", filters);
 
             if (options.SuggestedStartLocation?.TryGetLocalPath()  is { } folderPath)
-                chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0"))));
-            chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)));
+                chooserOptions.Add("current_folder", Variant.FromArray(new Array<byte>(Encoding.UTF8.GetBytes(folderPath + "\0"))));
+
+            chooserOptions.Add("multiple", new Variant(options.AllowMultiple));
 
             objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
 
@@ -77,7 +78,7 @@ namespace Avalonia.FreeDesktop
                 if (e is not null)
                     tsc.TrySetException(e);
                 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>();
@@ -88,15 +89,14 @@ namespace Avalonia.FreeDesktop
         {
             var parentWindow = $"x11:{_handle.Handle:X}";
             ObjectPath objectPath;
-            var chooserOptions = new Dictionary<string, DBusVariantItem>();
-            var filters = ParseFilters(options.FileTypeChoices);
-            if (filters is not null)
+            var chooserOptions = new Dictionary<string, Variant>();
+            if (TryParseFilters(options.FileTypeChoices, out var filters))
                 chooserOptions.Add("filters", filters);
 
             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)
-                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);
             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) =>
             {
                 if (e is not null)
+                {
                     tsc.TrySetException(e);
+                }
                 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;
-                        if(currentFilter != null)
+                        var name = currentFilter.GetItem(0).GetString();
+                        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() ?? "";
-                            selectedType = new FilePickerFileType(name);
-                            if(currentFilter[1] is DBusArrayItem types)
-                            {
-                                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;
-                            }
+                            if (t.GetItem(0).GetUInt32() == 1)
+                                mimeTypes.Add(t.GetItem(1).GetString());
+                            else
+                                patterns.Add(t.GetItem(1).GetString());
                         }
+
+                        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>();
 
             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)) },
-                { "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) }
+                { "directory", new Variant(true) },
+                { "multiple", new Variant(options.AllowMultiple) }
             };
 
             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)
-                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 request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
@@ -179,7 +169,7 @@ namespace Avalonia.FreeDesktop
                 if (e is not null)
                     tsc.TrySetException(e);
                 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>();
@@ -190,40 +180,35 @@ namespace Avalonia.FreeDesktop
                 .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 MimeStyle = 1u;
 
             // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])]
             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)
             {
-                var extensions = new List<DBusItem>();
+                var extensions = new List<Struct<uint, string>>();
                 if (fileType.Patterns?.Count > 0)
-                    extensions.AddRange(
-                        fileType.Patterns.Select(static pattern =>
-                            new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) })));
+                    extensions.AddRange(fileType.Patterns.Select(static pattern => Struct.Create(GlobStyle, pattern)));
                 else if (fileType.MimeTypes?.Count > 0)
-                    extensions.AddRange(
-                        fileType.MimeTypes.Select(static mimeType =>
-                            new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) })));
+                    extensions.AddRange(fileType.MimeTypes.Select(static mimeType => Struct.Create(MimeStyle, mimeType)));
                 else
                     continue;
 
-                filters.Add(new DBusStructItem(
-                    new DBusItem[]
-                    {
-                        new DBusStringItem(fileType.Name),
-                        new DBusArrayItem(DBusType.Struct, extensions)
-                    }));
+                filters.Add(Struct.Create(fileType.Name, new Array<Struct<uint, string>>(extensions)));
             }
 
-            return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", new DBusArrayItem(DBusType.Struct, filters)) : null;
+            result = Variant.FromArray(filters);
+            return true;
         }
     }
 }

+ 22 - 34
src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

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

+ 3 - 0
src/Avalonia.X11/X11Window.cs

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