From d7904b34cd7995935b4dce91bc057c5480973031 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 29 Dec 2022 20:55:14 +0100 Subject: [PATCH 01/36] Use Tmds.DBus.Protocol --- .gitmodules | 3 + Avalonia.Desktop.slnf | 7 +- Avalonia.sln | 7 + NuGet.Config | 1 + .../AppMenuRegistrar.DBus.cs | 176 ++++ .../Avalonia.FreeDesktop.csproj | 3 +- src/Avalonia.FreeDesktop/DBus.DBus.cs | 643 ++++++++++++ src/Avalonia.FreeDesktop/DBusFileChooser.cs | 32 - src/Avalonia.FreeDesktop/DBusHelper.cs | 54 +- .../DBusIme/DBusTextInputMethodBase.cs | 58 +- .../DBusIme/Fcitx/Fcitx.DBus.cs | 627 ++++++++++++ .../DBusIme/Fcitx/FcitxDBus.cs | 69 -- .../DBusIme/Fcitx/FcitxICWrapper.cs | 32 +- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 47 +- .../DBusIme/IBus/IBus.DBus.cs | 513 ++++++++++ .../DBusIme/IBus/IBusDBus.cs | 52 - .../DBusIme/IBus/IBusEnums.cs | 2 +- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 44 +- .../DBusIme/X11DBusImeHelper.cs | 25 +- src/Avalonia.FreeDesktop/DBusMenu.DBus.cs | 463 +++++++++ src/Avalonia.FreeDesktop/DBusMenu.cs | 56 -- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 432 ++++---- src/Avalonia.FreeDesktop/DBusRequest.cs | 16 - src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 107 +- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 415 ++++---- .../FreeDesktopPortalDesktop.DBus.cs | 937 ++++++++++++++++++ .../StatusNotifierWatcher.DBus.cs | 351 +++++++ src/Avalonia.X11/X11Window.cs | 6 +- src/Linux/Tmds.DBus | 1 + 29 files changed, 4272 insertions(+), 907 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBus.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusFileChooser.cs create mode 100644 src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBusMenu.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusMenu.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusRequest.cs create mode 100644 src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs create mode 100644 src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs create mode 160000 src/Linux/Tmds.DBus diff --git a/.gitmodules b/.gitmodules index 032bc879cc..16bc977251 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "nukebuild/il-repack"] path = nukebuild/il-repack url = https://github.com/Gillibald/il-repack +[submodule "Tmds.DBus"] + path = src/Linux/Tmds.DBus + url = https://github.com/affederaffe/Tmds.DBus diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3fa8e969c8..72eb13d0a9 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -7,9 +7,9 @@ "samples\\ControlCatalog\\ControlCatalog.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", - "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", @@ -31,15 +31,16 @@ "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", + "src\\Linux\\Tmds.DBus\\src\\Tmds.DBus.Protocol\\Tmds.DBus.Protocol.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", + "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", + "src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", - "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", - "src\\tools\\DevGenerators\\DevGenerators.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index fc42a5d63b..e399483896 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,6 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.Protocol", "src\Linux\Tmds.DBus\src\Tmds.DBus.Protocol\Tmds.DBus.Protocol.csproj", "{29E25263-3CC3-4D55-A042-00BA136867D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +544,10 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -606,6 +612,7 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {29E25263-3CC3-4D55-A042-00BA136867D4} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/NuGet.Config b/NuGet.Config index 7d2bd8abd2..7a9dd7993c 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,5 +6,6 @@ + diff --git a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs new file mode 100644 index 0000000000..a45d1642fc --- /dev/null +++ b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs @@ -0,0 +1,176 @@ +using System; +using System.Threading.Tasks; + +using Tmds.DBus.Protocol; + + +namespace Avalonia.FreeDesktop +{ + internal class Registrar : AppMenuRegistrarObject + { + private const string Interface = "com.canonical.AppMenu.Registrar"; + + public Registrar(RegistrarService service, ObjectPath path) : base(service, path) { } + + public Task RegisterWindowAsync(uint windowId, ObjectPath menuObjectPath) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uo", + member: "RegisterWindow", + flags: MessageFlags.NoReplyExpected); + writer.WriteUInt32(windowId); + writer.WriteObjectPath(menuObjectPath); + return writer.CreateMessage(); + } + } + + public Task UnregisterWindowAsync(uint windowId) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "UnregisterWindow"); + writer.WriteUInt32(windowId); + return writer.CreateMessage(); + } + } + + public Task<(string Service, ObjectPath MenuObjectPath)> GetMenuForWindowAsync(uint windowId) + { + return Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "GetMenuForWindow"); + writer.WriteUInt32(windowId); + return writer.CreateMessage(); + } + } + } + + internal class RegistrarService + { + public RegistrarService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public Registrar CreateRegistrar(string path) => new Registrar(this, path); + } + + internal class AppMenuRegistrarObject + { + protected AppMenuRegistrarObject(RegistrarService service, ObjectPath path) + => (Service, Path) = (service, path); + + public RegistrarService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (Exception? ex, PropertyChanges changes, object? rs, object? hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (Exception? ex, TArg arg, object? rs, object? hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, (Message message, object? state) => null!, + (Exception? ex, object v, object? rs, object? hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static (string, ObjectPath) ReadMessage_so(Message message, AppMenuRegistrarObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadObjectPath(); + return (arg0, arg1); + } + } +} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 3b1c6cc7b1..a33fa7b32d 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -12,7 +12,8 @@ - + + diff --git a/src/Avalonia.FreeDesktop/DBus.DBus.cs b/src/Avalonia.FreeDesktop/DBus.DBus.cs new file mode 100644 index 0000000000..4cf2b76b45 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBus.DBus.cs @@ -0,0 +1,643 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Tmds.DBus.Protocol; + + +namespace Avalonia.FreeDesktop +{ + internal record DBusProperties + { + public string[] Features { get; set; } = default!; + public string[] Interfaces { get; set; } = default!; + } + + internal class DBus : DBusObject + { + private const string Interface = "org.freedesktop.DBus"; + + public DBus(DBusService service, ObjectPath path) : base(service, path) { } + + public Task HelloAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Hello"); + return writer.CreateMessage(); + } + } + + public Task RequestNameAsync(string a0, uint a1) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "su", + member: "RequestName"); + writer.WriteString(a0); + writer.WriteUInt32(a1); + return writer.CreateMessage(); + } + } + + public Task ReleaseNameAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "ReleaseName"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task StartServiceByNameAsync(string a0, uint a1) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "su", + member: "StartServiceByName"); + writer.WriteString(a0); + writer.WriteUInt32(a1); + return writer.CreateMessage(); + } + } + + public Task UpdateActivationEnvironmentAsync(Dictionary a0) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "a{ss}", + member: "UpdateActivationEnvironment"); + writer.WriteDictionary(a0); + return writer.CreateMessage(); + } + } + + public Task NameHasOwnerAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "NameHasOwner"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task ListNamesAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "ListNames"); + return writer.CreateMessage(); + } + } + + public Task ListActivatableNamesAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "ListActivatableNames"); + return writer.CreateMessage(); + } + } + + public Task AddMatchAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "AddMatch"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task RemoveMatchAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RemoveMatch"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetNameOwnerAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetNameOwner"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task ListQueuedOwnersAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "ListQueuedOwners"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetConnectionUnixUserAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionUnixUser"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetConnectionUnixProcessIDAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionUnixProcessID"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetAdtAuditSessionDataAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetAdtAuditSessionData"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetConnectionSELinuxSecurityContextAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionSELinuxSecurityContext"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task ReloadConfigAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "ReloadConfig"); + return writer.CreateMessage(); + } + } + + public Task GetIdAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "GetId"); + return writer.CreateMessage(); + } + } + + public Task> GetConnectionCredentialsAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_aesv(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionCredentials"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public ValueTask WatchNameOwnerChangedAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "NameOwnerChanged", static (m, s) => + ReadMessage_sss(m, (DBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchNameLostAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "NameLost", static (m, s) => + ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchNameAcquiredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "NameAcquired", static (m, s) => + ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); + + public Task SetFeaturesAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Features"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + + public Task SetInterfacesAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Interfaces"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + + public Task GetFeaturesAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Features"), static (m, s) => + ReadMessage_v_as(m, (DBusObject)s!), this); + + public Task GetInterfacesAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Interfaces"), static (m, s) => + ReadMessage_v_as(m, (DBusObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) => + ReadMessage(m, (DBusObject)s!), this); + + static DBusProperties ReadMessage(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, static (m, s) => + ReadMessage(m, (DBusObject)s!), handler, emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + var headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new List(); + var property = reader.ReadString(); + switch (property) + { + case "Features": + invalidated.Add("Features"); + break; + case "Interfaces": + invalidated.Add("Interfaces"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static DBusProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new DBusProperties(); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Features": + reader.ReadSignature("as"); + props.Features = reader.ReadArray(); + changedList?.Add("Features"); + break; + case "Interfaces": + reader.ReadSignature("as"); + props.Interfaces = reader.ReadArray(); + changedList?.Add("Interfaces"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal class DBusService + { + public DBusService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public DBus CreateDBus(string path) => new(this, path); + } + + internal class DBusObject + { + protected DBusObject(DBusService service, ObjectPath path) + { + Service = service; + Path = path; + } + + public DBusService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => + ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static string ReadMessage_s(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static uint ReadMessage_u(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadUInt32(); + } + + protected static bool ReadMessage_b(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + + protected static string[] ReadMessage_as(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray(); + } + + protected static byte[] ReadMessage_ay(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray(); + } + + protected static Dictionary ReadMessage_aesv(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadDictionary(); + } + + protected static (string, string, string) ReadMessage_sss(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadString(); + return (arg0, arg1, arg2); + } + + protected static string[] ReadMessage_v_as(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArray(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusFileChooser.cs b/src/Avalonia.FreeDesktop/DBusFileChooser.cs deleted file mode 100644 index 24db614a02..0000000000 --- a/src/Avalonia.FreeDesktop/DBusFileChooser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.FileChooser")] - internal interface IFileChooser : IDBusObject - { - Task OpenFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFilesAsync(string ParentWindow, string Title, IDictionary Options); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - internal class FileChooserProperties - { - public uint Version { get; set; } - } - - internal static class FileChooserExtensions - { - public static Task GetVersionAsync(this IFileChooser o) => o.GetAsync("version"); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index ef99838208..e0445cc95c 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -1,51 +1,12 @@ using System; using System.Threading; using Avalonia.Logging; -using Avalonia.Threading; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { public static class DBusHelper { - /// - /// This class uses synchronous execution at DBus connection establishment stage - /// then switches to using AvaloniaSynchronizationContext - /// - private class DBusSyncContext : SynchronizationContext - { - private readonly object _lock = new(); - private SynchronizationContext? _ctx; - - public override void Post(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Post(d, state); - else - d(state); - } - } - - public override void Send(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Send(d, state); - else - d(state); - } - } - - public void Initialized() - { - lock (_lock) - _ctx = new AvaloniaSynchronizationContext(); - } - } - public static Connection? Connection { get; private set; } public static Connection? TryInitialize(string? dbusAddress = null) @@ -56,19 +17,14 @@ namespace Avalonia.FreeDesktop var oldContext = SynchronizationContext.Current; try { - - var dbusContext = new DBusSyncContext(); - SynchronizationContext.SetSynchronizationContext(dbusContext); - var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) + var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!) { - AutoConnect = false, - SynchronizationContext = dbusContext + AutoConnect = false }); + // Connect synchronously - conn.ConnectAsync().Wait(); + conn.ConnectAsync().GetAwaiter().GetResult(); - // Initialize a brand new sync-context - dbusContext.Initialized(); Connection = conn; } catch (Exception e) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 899288b4a8..c7bf530cfd 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme { @@ -46,7 +46,7 @@ namespace Avalonia.FreeDesktop.DBusIme public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { - _queue = new DBusCallQueue(QueueOnError); + _queue = new DBusCallQueue(QueueOnErrorAsync); Connection = connection; _knownNames = knownNames; Watch(); @@ -54,12 +54,17 @@ namespace Avalonia.FreeDesktop.DBusIme public ITextInputMethodClient Client => _client; - public bool IsActive => _client != null; + public bool IsActive => _client is not null; - async void Watch() + private async void Watch() { foreach (var name in _knownNames) - _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); + { + var dbus = new DBusService(Connection, name).CreateDBus("/org/freedesktop/DBus"); + _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); + var nameOwner = await dbus.GetNameOwnerAsync(name); + OnNameChange(null, (name, null, nameOwner)); + } } protected abstract Task Connect(string name); @@ -67,9 +72,9 @@ namespace Avalonia.FreeDesktop.DBusIme protected string GetAppName() => Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - private async void OnNameChange(ServiceOwnerChangedEventArgs args) + private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args) { - if (args.NewOwner != null && _currentName == null) + if (args.NewOwner is not null && _currentName is null) { _onlineNamesQueue.Enqueue(args.ServiceName); if (!_connecting) @@ -89,10 +94,10 @@ namespace Avalonia.FreeDesktop.DBusIme return; } } - catch (Exception e) + catch (Exception ex) { Logger.TryGet(LogEventLevel.Error, "IME") - ?.Log(this, "Unable to create IME input context:\n" + e); + ?.Log(this, "Unable to create IME input context:\n" + ex); } } } @@ -105,7 +110,7 @@ namespace Avalonia.FreeDesktop.DBusIme } // IME has crashed - if (args.NewOwner == null && args.ServiceName == _currentName) + if (args.NewOwner is null && args.ServiceName == _currentName) { _currentName = null; foreach (var s in _disposables) @@ -120,7 +125,7 @@ namespace Avalonia.FreeDesktop.DBusIme } } - protected virtual Task Disconnect() + protected virtual Task DisconnectAsync() { return Task.CompletedTask; } @@ -136,13 +141,13 @@ namespace Avalonia.FreeDesktop.DBusIme _imeActive = null; } - async Task QueueOnError(Exception e) + private async Task QueueOnErrorAsync(Exception e) { Logger.TryGet(LogEventLevel.Error, "IME") ?.Log(this, "Error:\n" + e); try { - await Disconnect(); + await DisconnectAsync(); } catch (Exception ex) { @@ -160,20 +165,13 @@ namespace Avalonia.FreeDesktop.DBusIme if(d is { }) _disposables.Add(d); } - + public void Dispose() { foreach(var d in _disposables) d.Dispose(); _disposables.Clear(); - try - { - Disconnect().ContinueWith(_ => { }); - } - catch - { - // fire and forget - } + DisconnectAsync(); _currentName = null; } @@ -182,13 +180,13 @@ namespace Avalonia.FreeDesktop.DBusIme protected abstract Task ResetContextCore(); protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); - void UpdateActive() + private void UpdateActive() { _queue.Enqueue(async () => { if(!IsConnected) return; - + var active = _windowActive && _controlActive; if (active != _imeActive) { @@ -204,7 +202,7 @@ namespace Avalonia.FreeDesktop.DBusIme _windowActive = active; UpdateActive(); } - + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _client = client; @@ -227,7 +225,7 @@ namespace Avalonia.FreeDesktop.DBusIme // Error, disconnect catch (Exception e) { - await QueueOnError(e); + await QueueOnErrorAsync(e); return false; } } @@ -240,7 +238,7 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireCommit(string s) => _onCommit?.Invoke(s); - + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { @@ -249,8 +247,8 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); - - void UpdateCursorRect() + + private void UpdateCursorRect() { _queue.Enqueue(async () => { @@ -265,7 +263,7 @@ namespace Avalonia.FreeDesktop.DBusIme } }); } - + void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) { _windowPosition = position; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs new file mode 100644 index 0000000000..f9e4497892 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs @@ -0,0 +1,627 @@ +using System; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + internal class InputContext : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputContext"; + + public InputContext(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task FocusInAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusIn"); + return writer.CreateMessage(); + } + } + + public Task FocusOutAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusOut"); + return writer.CreateMessage(); + } + } + + public Task ResetAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Reset"); + return writer.CreateMessage(); + } + } + + public Task SetCursorRectAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorRect"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task SetCapacityAsync(uint caps) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "SetCapacity"); + writer.WriteUInt32(caps); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "suu", + member: "SetSurroundingText"); + writer.WriteString(text); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uu", + member: "SetSurroundingTextPosition"); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task DestroyICAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "DestroyIC"); + return writer.CreateMessage(); + } + } + + public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, int type, uint time) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_i(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uuuiu", + member: "ProcessKeyEvent"); + writer.WriteUInt32(keyval); + writer.WriteUInt32(keycode); + writer.WriteUInt32(state); + writer.WriteInt32(type); + writer.WriteUInt32(time); + return writer.CreateMessage(); + } + } + + public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => + ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchCurrentIMAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => + ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => + ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchForwardKeyAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => + ReadMessage_uui(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchDeleteSurroundingTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => + ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); + } + + internal class InputMethod : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputMethod"; + + public InputMethod(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task<(int Icid, bool Enable, uint Keyval1, uint State1, uint Keyval2, uint State2)> CreateICv3Async(string appname, int pid) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ibuuuu(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "si", + member: "CreateICv3"); + writer.WriteString(appname); + writer.WriteInt32(pid); + return writer.CreateMessage(); + } + } + } + + internal class InputContext1 : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputContext1"; + + public InputContext1(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task FocusInAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusIn"); + return writer.CreateMessage(); + } + } + + public Task FocusOutAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusOut"); + return writer.CreateMessage(); + } + } + + public Task ResetAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Reset"); + return writer.CreateMessage(); + } + } + + public Task SetCursorRectAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorRect"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task SetCapabilityAsync(ulong caps) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "t", + member: "SetCapability"); + writer.WriteUInt64(caps); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "suu", + member: "SetSurroundingText"); + writer.WriteString(text); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uu", + member: "SetSurroundingTextPosition"); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task DestroyICAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "DestroyIC"); + return writer.CreateMessage(); + } + } + + public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, bool type, uint time) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uuubu", + member: "ProcessKeyEvent"); + writer.WriteUInt32(keyval); + writer.WriteUInt32(keycode); + writer.WriteUInt32(state); + writer.WriteBool(type); + writer.WriteUInt32(time); + return writer.CreateMessage(); + } + } + + public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => + ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchCurrentIMAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => + ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => + ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchForwardKeyAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => + ReadMessage_uub(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchDeleteSurroundingTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => + ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); + } + + internal class InputMethod1 : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputMethod1"; + + public InputMethod1(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task<(ObjectPath A0, byte[] A1)> CreateInputContextAsync((string, string)[] a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_oay(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "a(ss)", + member: "CreateInputContext"); + writer.WriteArray(a0); + return writer.CreateMessage(); + } + } + } + + internal class FcitxService + { + public FcitxService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public InputContext CreateInputContext(string path) => new(this, path); + public InputMethod CreateInputMethod(string path) => new(this, path); + public InputContext1 CreateInputContext1(string path) => new(this, path); + public InputMethod1 CreateInputMethod1(string path) => new(this, path); + } + + internal class FcitxObject + { + protected FcitxObject(FcitxService service, ObjectPath path) + => (Service, Path) = (service, path); + + public FcitxService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) => + null!, static (ex, _, _, hs) => + ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static int ReadMessage_i(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadInt32(); + } + + protected static string ReadMessage_s(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static (string, string, string) ReadMessage_sss(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadString(); + return (arg0, arg1, arg2); + } + + protected static ((string, int)[], int) ReadMessage_arsizi(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadArray<(string, int)>(); + var arg1 = reader.ReadInt32(); + return (arg0, arg1); + } + + protected static (uint, uint, int) ReadMessage_uui(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadInt32(); + return (arg0, arg1, arg2); + } + + protected static (int, uint) ReadMessage_iu(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadUInt32(); + return (arg0, arg1); + } + + protected static (int, bool, uint, uint, uint, uint) ReadMessage_ibuuuu(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadBool(); + var arg2 = reader.ReadUInt32(); + var arg3 = reader.ReadUInt32(); + var arg4 = reader.ReadUInt32(); + var arg5 = reader.ReadUInt32(); + return (arg0, arg1, arg2, arg3, arg4, arg5); + } + + protected static bool ReadMessage_b(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + + protected static (uint, uint, bool) ReadMessage_uub(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadBool(); + return (arg0, arg1, arg2); + } + + protected static (ObjectPath, byte[]) ReadMessage_oay(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadObjectPath(); + var arg1 = reader.ReadArray(); + return (arg0, arg1); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs deleted file mode 100644 index 06afacaa29..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.Fcitx -{ - [DBusInterface("org.fcitx.Fcitx.InputMethod")] - interface IFcitxInputMethod : IDBusObject - { - Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( - string Appname, int Pid); - } - - - [DBusInterface("org.fcitx.Fcitx.InputContext")] - interface IFcitxInputContext : IDBusObject - { - Task EnableICAsync(); - Task CloseICAsync(); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task MouseEventAsync(int X); - Task SetCursorLocationAsync(int X, int Y); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapacityAsync(uint Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action? onError = null); - Task WatchCloseIMAsync(Action handler, Action? onError = null); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputContext1")] - interface IFcitxInputContext1 : IDBusObject - { - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapabilityAsync(ulong Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputMethod1")] - interface IFcitxInputMethod1 : IDBusObject - { - Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 6c503edb41..b71ca55cf8 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,15 +5,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1? _modern; - private readonly IFcitxInputContext? _old; + private readonly InputContext1? _modern; + private readonly InputContext? _old; - public FcitxICWrapper(IFcitxInputContext old) + public FcitxICWrapper(InputContext old) { _old = old; } - public FcitxICWrapper(IFcitxInputContext1 modern) + public FcitxICWrapper(InputContext1 modern) { _modern = modern; } @@ -21,32 +21,30 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { - if(_old!=null) + if (_old is not null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) - ?? _modern?.WatchCommitStringAsync(handler) - ?? Task.FromResult(default(IDisposable?)); + public ValueTask WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? new ValueTask(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) - { - return _old?.WatchForwardKeyAsync(handler) - ?? _modern?.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))) - ?? Task.FromResult(default(IDisposable?)); - } + public ValueTask WatchForwardKeyAsync(Action handler) => + _old?.WatchForwardKeyAsync(handler) + ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0))) + ?? new ValueTask(default(IDisposable?)); public Task SetCapacityAsync(uint flags) => _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 791431dfa7..2a5260b1cf 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,12 +1,10 @@ using System; using System.Diagnostics; -using System.Reactive.Concurrency; -using System.Reflection; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { @@ -15,32 +13,25 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; - public FcitxX11TextInputMethod(Connection connection) : base(connection, - "org.fcitx.Fcitx", - "org.freedesktop.portal.Fcitx" - ) - { - - } + public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } protected override async Task Connect(string name) { + var service = new FcitxService(Connection, name); if (name == "org.fcitx.Fcitx") { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = service.CreateInputMethod("/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = Connection.CreateProxy(name, - "/inputcontext_" + resp.icid); - + var proxy = service.CreateInputContext($"/inputcontext_{resp.Icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = service.CreateInputMethod1("/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = Connection.CreateProxy(name, resp.path); + var proxy = service.CreateInputContext1(resp.A0); _context = new FcitxICWrapper(proxy); } @@ -49,7 +40,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -64,14 +55,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Math.Max(1, cursorRect.Height)) ?? Task.CompletedTask; - protected override Task SetActiveCore(bool active)=> (active + protected override Task SetActiveCore(bool active)=> (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) ?? Task.CompletedTask; - - protected override Task ResetContextCore() => _context?.ResetAsync() - ?? Task.CompletedTask; + protected override Task ResetContextCore() => _context?.ResetAsync() ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -88,17 +77,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - if (_context is { }) - { + if (_context is not null) return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, (uint)args.Timestamp).ConfigureAwait(false); - } - else - { - return false; - } + + return false; } - + public override void SetOptions(TextInputOptions options) => Enqueue(async () => { @@ -128,7 +113,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } }); - private void OnForward((uint keyval, uint state, int type) ev) + private void OnForward(Exception? e, (uint keyval, uint state, int type) ev) { var state = (FcitxKeyState)ev.state; KeyModifiers mods = default; @@ -150,6 +135,6 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(string s) => FireCommit(s); + private void OnCommitString(Exception? e, string s) => FireCommit(s); } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs new file mode 100644 index 0000000000..e0f9681766 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs @@ -0,0 +1,513 @@ +using System; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + internal class Portal : IBusObject + { + private const string Interface = "org.freedesktop.IBus.Portal"; + + public Portal(IBusService service, ObjectPath path) : base(service, path) { } + + public Task CreateInputContextAsync(string clientName) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (IBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "CreateInputContext"); + writer.WriteString(clientName); + return writer.CreateMessage(); + } + } + } + + internal class InputContext : IBusObject + { + private const string Interface = "org.freedesktop.IBus.InputContext"; + + public InputContext(IBusService service, ObjectPath path) : base(service, path) { } + + public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (IBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uuu", + member: "ProcessKeyEvent"); + writer.WriteUInt32(keyval); + writer.WriteUInt32(keycode); + writer.WriteUInt32(state); + return writer.CreateMessage(); + } + } + + public Task SetCursorLocationAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorLocation"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task SetCursorLocationRelativeAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorLocationRelative"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task ProcessHandWritingEventAsync(double[] coordinates) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ad", + member: "ProcessHandWritingEvent"); + writer.WriteArray(coordinates); + return writer.CreateMessage(); + } + } + + public Task CancelHandWritingAsync(uint nStrokes) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "CancelHandWriting"); + writer.WriteUInt32(nStrokes); + return writer.CreateMessage(); + } + } + + public Task FocusInAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusIn"); + return writer.CreateMessage(); + } + } + + public Task FocusOutAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusOut"); + return writer.CreateMessage(); + } + } + + public Task ResetAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Reset"); + return writer.CreateMessage(); + } + } + + public Task SetCapabilitiesAsync(uint caps) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "SetCapabilities"); + writer.WriteUInt32(caps); + return writer.CreateMessage(); + } + } + + public Task PropertyActivateAsync(string name, uint state) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "su", + member: "PropertyActivate"); + writer.WriteString(name); + writer.WriteUInt32(state); + return writer.CreateMessage(); + } + } + + public Task SetEngineAsync(string name) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "SetEngine"); + writer.WriteString(name); + return writer.CreateMessage(); + } + } + + public Task GetEngineAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_v(m, (IBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "GetEngine"); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextAsync(object text, uint cursorPos, uint anchorPos) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "vuu", + member: "SetSurroundingText"); + writer.WriteVariant(text); + writer.WriteUInt32(cursorPos); + writer.WriteUInt32(anchorPos); + return writer.CreateMessage(); + } + } + + public ValueTask WatchCommitTextAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CommitText", static (m, s) => + ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchForwardKeyEventAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKeyEvent", static (m, s) => + ReadMessage_uuu(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdatePreeditTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditText", static (m, s) => + ReadMessage_vub(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdatePreeditTextWithModeAsync( + Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditTextWithMode", static (m, s) => + ReadMessage_vubu(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchShowPreeditTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ShowPreeditText", handler, emitOnCapturedContext); + + public ValueTask WatchHidePreeditTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "HidePreeditText", handler, emitOnCapturedContext); + + public ValueTask WatchUpdateAuxiliaryTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateAuxiliaryText", static (m, s) => + ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchShowAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ShowAuxiliaryText", handler, emitOnCapturedContext); + + public ValueTask WatchHideAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "HideAuxiliaryText", handler, emitOnCapturedContext); + + public ValueTask WatchUpdateLookupTableAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateLookupTable", static (m, s) => + ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchShowLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ShowLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchHideLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "HideLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchPageUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "PageUpLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchPageDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "PageDownLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchCursorUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "CursorUpLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchCursorDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "CursorDownLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchRegisterPropertiesAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "RegisterProperties", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdatePropertyAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "UpdateProperty", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); + } + + internal class Service : IBusObject + { + private const string Interface = "org.freedesktop.IBus.Service"; + + public Service(IBusService service, ObjectPath path) : base(service, path) { } + + public Task DestroyAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Destroy"); + return writer.CreateMessage(); + } + } + } + + internal class IBusService + { + public IBusService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public Portal CreatePortal(string path) => new(this, path); + public InputContext CreateInputContext(string path) => new(this, path); + public Service CreateService(string path) => new(this, path); + } + + internal class IBusObject + { + protected IBusObject(IBusService service, ObjectPath path) + => (Service, Path) = (service, path); + + public IBusService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static ObjectPath ReadMessage_o(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadObjectPath(); + } + + protected static bool ReadMessage_b(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + + protected static object ReadMessage_v(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadVariant(); + } + + protected static (uint, uint, uint) ReadMessage_uuu(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadUInt32(); + return (arg0, arg1, arg2); + } + + protected static (object, uint, bool) ReadMessage_vub(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadBool(); + return (arg0, arg1, arg2); + } + + protected static (object, uint, bool, uint) ReadMessage_vubu(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadBool(); + var arg3 = reader.ReadUInt32(); + return (arg0, arg1, arg2, arg3); + } + + protected static (object, bool) ReadMessage_vb(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadBool(); + return (arg0, arg1); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs deleted file mode 100644 index 4ef034adb9..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.IBus -{ - [DBusInterface("org.freedesktop.IBus.InputContext")] - interface IIBusInputContext : IDBusObject - { - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); - Task SetCursorLocationAsync(int X, int Y, int W, int H); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCapabilitiesAsync(uint Caps); - Task PropertyActivateAsync(string Name, int State); - Task SetEngineAsync(string Name); - Task GetEngineAsync(); - Task DestroyAsync(); - Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action? onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); - Task WatchShowLookupTableAsync(Action handler, Action? onError = null); - Task WatchHideLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); - } - - - [DBusInterface("org.freedesktop.IBus.Portal")] - interface IIBusPortal : IDBusObject - { - Task CreateInputContextAsync(string Name); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs index 3070f51a8e..b8ac816694 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -18,7 +18,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Button3Mask = 1 << 10, Button4Mask = 1 << 11, Button5Mask = 1 << 12, - + HandledMask = 1 << 24, ForwardMask = 1 << 25, IgnoredMask = ForwardMask, diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 2324ca44a7..f3ab8617a4 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext? _context; + private Service? _service; + private InputContext? _context; - public IBusX11TextInputMethod(Connection connection) : base(connection, - "org.freedesktop.portal.IBus") - { - } + public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var path = - await Connection.CreateProxy(name, "/org/freedesktop/IBus") - .CreateInputContextAsync(GetAppName()); - - _context = Connection.CreateProxy(name, path); + var service = new IBusService(Connection, name); + var path = await service.CreatePortal("/org/freedesktop/IBus").CreateInputContextAsync(GetAppName()); + _context = service.CreateInputContext(path); + _service = service.CreateService(path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); return true; } - private void OnForwardKey((uint keyval, uint keycode, uint state) k) + private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { var state = (IBusModifierMask)k.state; KeyModifiers mods = default; @@ -49,8 +46,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus }); } - - private void OnCommitText(object wtf) + private void OnCommitText(Exception? e, object wtf) { // Hello darkness, my old friend if (wtf.GetType().GetField("Item3") is { } prop) @@ -61,16 +57,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus } } - protected override Task Disconnect() => _context?.DestroyAsync() - ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; protected override void OnDisconnected() { + _service = null; _context = null; base.OnDisconnected(); } - protected override Task SetCursorRectCore(PixelRect rect) + protected override Task SetCursorRectCore(PixelRect rect) => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) ?? Task.CompletedTask; @@ -96,20 +92,12 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - if(_context is { }) - { - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); - } - else - { - return Task.FromResult(false); - } - + return _context is not null ? _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state) : Task.FromResult(false); } public override void SetOptions(TextInputOptions options) { - // No-op, because ibus + // No-op, because ibus } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index 86978c8b60..c68e10043b 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -2,44 +2,43 @@ using System; using System.Collections.Generic; using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.FreeDesktop.DBusIme.IBus; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme { public class X11DBusImeHelper { - private static readonly Dictionary> KnownMethods = - new Dictionary> + private static readonly Dictionary> KnownMethods = new() { - ["fcitx"] = conn => + ["fcitx"] = static conn => new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), - ["ibus"] = conn => + ["ibus"] = static conn => new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - - static Func? DetectInputMethod() + + private static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { var value = Environment.GetEnvironmentVariable(name); - + if (value == "none") return null; - - if (value != null && KnownMethods.TryGetValue(value, out var factory)) + + if (value is not null && KnownMethods.TryGetValue(value, out var factory)) return factory; } return null; } - + public static bool DetectAndRegister() { var factory = DetectInputMethod(); - if (factory != null) + if (factory is not null) { var conn = DBusHelper.TryInitialize(); - if (conn != null) + if (conn is not null) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); return true; diff --git a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs new file mode 100644 index 0000000000..d9016d8644 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs @@ -0,0 +1,463 @@ +using System; +using Tmds.DBus.Protocol; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop +{ + internal record DBusMenuProperties + { + public uint Version { get; set; } + public string TextDirection { get; set; } = default!; + public string Status { get; set; } = default!; + public string[] IconThemePath { get; set; } = default!; + } + + internal class DBusMenu : DBusMenuObject + { + private const string Interface = "com.canonical.dbusmenu"; + public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) + { } + public Task<(uint Revision, (int, Dictionary, object[]) Layout)> GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_uriaesvavz(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iias", + member: "GetLayout"); + writer.WriteInt32(parentId); + writer.WriteInt32(recursionDepth); + writer.WriteArray(propertyNames); + return writer.CreateMessage(); + } + } + public Task<(int, Dictionary)[]> GetGroupPropertiesAsync(int[] ids, string[] propertyNames) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ariaesvz(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "aias", + member: "GetGroupProperties"); + writer.WriteArray(ids); + writer.WriteArray(propertyNames); + return writer.CreateMessage(); + } + } + public Task GetPropertyAsync(int id, string name) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_v(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "is", + member: "GetProperty"); + writer.WriteInt32(id); + writer.WriteString(name); + return writer.CreateMessage(); + } + } + public Task EventAsync(int id, string eventId, object data, uint timestamp) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "isvu", + member: "Event"); + writer.WriteInt32(id); + writer.WriteString(eventId); + writer.WriteVariant(data); + writer.WriteUInt32(timestamp); + return writer.CreateMessage(); + } + } + public Task EventGroupAsync((int, string, object, uint)[] events) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ai(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "a(isvu)", + member: "EventGroup"); + writer.WriteArray(events); + return writer.CreateMessage(); + } + } + public Task AboutToShowAsync(int id) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_b(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "i", + member: "AboutToShow"); + writer.WriteInt32(id); + return writer.CreateMessage(); + } + } + public Task<(int[] UpdatesNeeded, int[] IdErrors)> AboutToShowGroupAsync(int[] ids) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_aiai(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ai", + member: "AboutToShowGroup"); + writer.WriteArray(ids); + return writer.CreateMessage(); + } + } + public ValueTask WatchItemsPropertiesUpdatedAsync(Action)[] UpdatedProps, (int, string[])[] RemovedProps)> handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", (m, s) => ReadMessage_ariaesvzariasz(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + public ValueTask WatchLayoutUpdatedAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", (m, s) => ReadMessage_ui(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + public ValueTask WatchItemActivationRequestedAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", (m, s) => ReadMessage_iu(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetTextDirectionAsync(string value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("TextDirection"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetStatusAsync(string value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Status"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetIconThemePathAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("IconThemePath"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), (m, s) => ReadMessage_v_u(m, (DBusMenuObject)s!), this); + public Task GetTextDirectionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + public Task GetStatusAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + public Task GetIconThemePathAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), (m, s) => ReadMessage_v_as(m, (DBusMenuObject)s!), this); + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), (m, s) => ReadMessage(m, (DBusMenuObject)s!), this); + static DBusMenuProperties ReadMessage(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + static PropertyChanges ReadMessage(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Version": invalidated.Add("Version"); break; + case "TextDirection": invalidated.Add("TextDirection"); break; + case "Status": invalidated.Add("Status"); break; + case "IconThemePath": invalidated.Add("IconThemePath"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static DBusMenuProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new DBusMenuProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + case "TextDirection": + reader.ReadSignature("s"); + props.TextDirection = reader.ReadString(); + changedList?.Add("TextDirection"); + break; + case "Status": + reader.ReadSignature("s"); + props.Status = reader.ReadString(); + changedList?.Add("Status"); + break; + case "IconThemePath": + reader.ReadSignature("as"); + props.IconThemePath = reader.ReadArray(); + changedList?.Add("IconThemePath"); + break; + default: + reader.ReadVariant(); + break; + } + } + return props; + } + } + + internal class DBusMenuService + { + public Connection Connection { get; } + public string Destination { get; } + public DBusMenuService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + public DBusMenu CreateDbusmenu(string path) => new DBusMenu(this, path); + } + + internal class DBusMenuObject + { + public DBusMenuService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + protected DBusMenuObject(DBusMenuService service, ObjectPath path) + => (Service, Path) = (service, path); + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, changes, rs, hs) => ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, (message, state) => null!, + (ex, v, rs, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadStruct, object[]>(); + return (arg0, arg1); + } + protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray<(int, Dictionary)>(); + } + protected static object ReadMessage_v(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadVariant(); + } + protected static int[] ReadMessage_ai(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray(); + } + protected static bool ReadMessage_b(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + protected static (int[], int[]) ReadMessage_aiai(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadArray(); + var arg1 = reader.ReadArray(); + return (arg0, arg1); + } + protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadArray<(int, Dictionary)>(); + var arg1 = reader.ReadArray<(int, string[])>(); + return (arg0, arg1); + } + protected static (uint, int) ReadMessage_ui(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadInt32(); + return (arg0, arg1); + } + protected static (int, uint) ReadMessage_iu(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadUInt32(); + return (arg0, arg1); + } + protected static uint ReadMessage_v_u(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("u"); + return reader.ReadUInt32(); + } + protected static string ReadMessage_v_s(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("s"); + return reader.ReadString(); + } + protected static string[] ReadMessage_v_as(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArray(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs deleted file mode 100644 index 3a1c65e7c9..0000000000 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusMenu -{ - - [DBusInterface("org.freedesktop.DBus.Properties")] - interface IFreeDesktopDBusProperties : IDBusObject - { - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [DBusInterface("com.canonical.dbusmenu")] - interface IDBusMenu : IFreeDesktopDBusProperties - { - Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); - Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); - Task GetPropertyAsync(int Id, string Name); - Task EventAsync(int Id, string EventId, object Data, uint Timestamp); - Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); - Task AboutToShowAsync(int Id); - Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); - } - - [Dictionary] - class DBusMenuProperties - { - public uint Version { get; set; } = default (uint); - public string? TextDirection { get; set; } = default (string); - public string? Status { get; set; } = default (string); - public string[]? IconThemePath { get; set; } = default (string[]); - } - - - [DBusInterface("com.canonical.AppMenu.Registrar")] - interface IRegistrar : IDBusObject - { - Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); - Task UnregisterWindowAsync(uint WindowId); - Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); - Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 657e324010..682434bc8d 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,98 +2,81 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using System.Reactive.Disposables; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; -using Tmds.DBus; +using Tmds.DBus.Protocol; + #pragma warning disable 1998 namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) - { - if (DBusHelper.Connection == null) - return null; + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) => + DBusHelper.Connection is null ? null : new DBusMenuExporterImpl(DBusHelper.Connection, xid); - return new DBusMenuExporterImpl(DBusHelper.Connection, xid); - } - - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) - { - return new DBusMenuExporterImpl(currentConection, path); - } + public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) => + new DBusMenuExporterImpl(currentConnection, path); - public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString("N"); + public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IMethodHandler, IDisposable { - private readonly Connection _dbus; + private readonly Connection _connection; + private readonly DBusMenuProperties _dBusMenuProperties = new() { Version = 2, Status = "normal" }; + private readonly Dictionary _idsToItems = new(); + private readonly Dictionary _itemsToIds = new(); + private readonly HashSet _menus = new(); private readonly uint _xid; - private IRegistrar? _registrar; + private readonly bool _appMenu = true; + private Registrar? _registrar; + private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; - private NativeMenu? _menu; - private readonly Dictionary _idsToItems = new Dictionary(); - private readonly Dictionary _itemsToIds = new Dictionary(); - private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; - private bool _appMenu = true; - - public DBusMenuExporterImpl(Connection dbus, IntPtr xid) + + public DBusMenuExporterImpl(Connection connection, IntPtr xid) { - _dbus = dbus; + _connection = connection; _xid = (uint)xid.ToInt32(); - ObjectPath = GenerateDBusMenuObjPath; + Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); Init(); } - public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + public DBusMenuExporterImpl(Connection connection, string path) { - _dbus = dbus; + _connection = connection; _appMenu = false; - ObjectPath = path; + Path = path; SetNativeMenu(new NativeMenu()); Init(); } - - async void Init() - { - try - { - if (_appMenu) - { - await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection?.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed && _registrar is { }) - await _registrar.RegisterWindowAsync(_xid, ObjectPath); - } - else - { - await _dbus.RegisterObjectAsync(this); - } - } - catch (Exception e) - { - Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) - ?.Log(this, e.Message); - // It's not really important if this code succeeds, - // and it's not important to know if it succeeds - // since even if we register the window it's not guaranteed that - // menu will be actually exported - } + public string Path { get; } + + private async void Init() + { + _connection.AddMethodHandler(this); + if (!_appMenu) + return; + var services = await _connection.ListServicesAsync(); + if (!services.Contains("com.canonical.AppMenu.Registrar")) + return; + _registrar = new RegistrarService(_connection, "com.canonical.AppMenu.Registrar") + .CreateRegistrar("/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registrar.RegisterWindowAsync(_xid, Path); + // It's not really important if this code succeeds, + // and it's not important to know if it succeeds + // since even if we register the window it's not guaranteed that + // menu will be actually exported } public void Dispose() @@ -101,7 +84,7 @@ namespace Avalonia.FreeDesktop if (_disposed) return; _disposed = true; - _dbus.UnregisterObject(this); + // Fire and forget _registrar?.UnregisterWindowAsync(_xid); } @@ -113,17 +96,17 @@ namespace Avalonia.FreeDesktop public void SetNativeMenu(NativeMenu? menu) { - if (menu == null) + if (menu is null) menu = new NativeMenu(); - if (_menu != null) + if (_menu is not null) ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; _menu = menu; ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; - + DoLayoutReset(); } - + /* This is basic initial implementation, so we don't actually track anything and just reset the whole layout on *ANY* change @@ -131,10 +114,10 @@ namespace Avalonia.FreeDesktop This is not how it should work and will prevent us from implementing various features, but that's the fastest way to get things working, so... */ - void DoLayoutReset() + private void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) + foreach (var i in _idsToItems.Values) i.PropertyChanged -= OnItemPropertyChanged; foreach(var menu in _menus) ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; @@ -142,10 +125,10 @@ namespace Avalonia.FreeDesktop _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; - LayoutUpdated?.Invoke((_revision, 0)); + EmitUIntIntSignal("LayoutUpdated", _revision, 0); } - void QueueReset() + private void QueueReset() { if(_resetQueued) return; @@ -163,10 +146,10 @@ namespace Avalonia.FreeDesktop private void EnsureSubscribed(NativeMenu? menu) { - if(menu!=null && _menus.Add(menu)) + if (menu is not null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; } - + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) @@ -190,33 +173,11 @@ namespace Avalonia.FreeDesktop QueueReset(); } - public ObjectPath ObjectPath { get; } - - - async Task IFreeDesktopDBusProperties.GetAsync(string prop) - { - if (prop == "Version") - return 2; - if (prop == "Status") - return "normal"; - return 0; - } - - async Task IFreeDesktopDBusProperties.GetAllAsync() - { - return new DBusMenuProperties - { - Version = 2, - Status = "normal", - }; - } - - private static string[] AllProperties = new[] - { + private static readonly string[] AllProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - - object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + + private object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; @@ -228,24 +189,20 @@ namespace Avalonia.FreeDesktop else if (it is NativeMenuItem item) { if (name == "type") - { return null; - } if (name == "label") - return item?.Header ?? ""; + return item.Header ?? ""; if (name == "enabled") { - if (item == null) - return null; - if (item.Menu != null && item.Menu.Items.Count == 0) + if (item.Menu is not null && item.Menu.Items.Count == 0) return false; - if (item.IsEnabled == false) + if (!item.IsEnabled) return false; return null; } if (name == "shortcut") { - if (item?.Gesture == null) + if (item.Gesture is null) return null; if (item.Gesture.KeyModifiers == 0) return null; @@ -271,19 +228,16 @@ namespace Avalonia.FreeDesktop return "radio"; } - if (name == "toggle-state") - { - if (item.ToggleType != NativeMenuItemToggleType.None) - return item.IsChecked ? 1 : 0; - } - + if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) + return item.IsChecked ? 1 : 0; + if (name == "icon-data") { - if (item.Icon != null) + if (item.Icon is not null) { var loader = AvaloniaLocator.Current.GetService(); - if (loader != null) + if (loader is not null) { var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); @@ -293,155 +247,199 @@ namespace Avalonia.FreeDesktop } } } - + if (name == "children-display") - return menu != null ? "submenu" : null; + return menu is not null ? "submenu" : null; } return null; } - private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { - if (names?.Length > 0 != true) + if (names.Length == 0) names = AllProperties; - _reusablePropertyList.Clear(); + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); - if (v != null) - _reusablePropertyList.Add(new KeyValuePair(n, v)); + if (v is not null) + properties.Add(n, v); } - return _reusablePropertyList.ToArray(); + return properties; } - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync( - int ParentId, int RecursionDepth, string[] PropertyNames) + private (int, Dictionary, object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { - var menu = GetMenu(ParentId); - var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); - if (!IsNativeMenuExported) - { - IsNativeMenuExported = true; - Dispatcher.UIThread.Post(() => - { - OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); - }); - } - return Task.FromResult(rv); - } - - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) - { - var id = item == null ? 0 : GetId(item); + var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = (depth == 0 || menu == null) ? Array.Empty() : new object[menu.Items.Count]; - if(menu != null) + var children = depth == 0 || menu is null ? Array.Empty() : new object[menu.Items.Count]; + if (menu is not null) + { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; - children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); } - - return (id, props, children); - } - - public Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) - { - var arr = new (int, KeyValuePair[])[Ids.Length]; - for (var c = 0; c < Ids.Length; c++) - { - var id = Ids[c]; - var item = GetMenu(id); - var props = GetProperties(item, PropertyNames); - arr[c] = (id, props); } - return Task.FromResult(arr); - } - - public async Task GetPropertyAsync(int Id, string Name) - { - return GetProperty(GetMenu(Id), Name) ?? 0; + return (id, props, children); } - - public void HandleEvent(int id, string eventId, object data, uint timestamp) + private void HandleEvent(int id, string eventId, object data, uint timestamp) { if (eventId == "clicked") { var item = GetMenu(id).item; - - if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) - { - if (menuItem?.IsEnabled == true) - bridge?.RaiseClicked(); - } + if (item is NativeMenuItem { IsEnabled: true } and INativeMenuItemExporterEventsImplBridge bridge) + bridge.RaiseClicked(); } } - - public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) - { - HandleEvent(Id, EventId, Data, Timestamp); - return Task.CompletedTask; - } - public Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) + public ValueTask HandleMethodAsync(MethodContext context) { - foreach (var e in Events) - HandleEvent(e.id, e.eventId, e.data, e.timestamp); - return Task.FromResult(Array.Empty()); - } - - public async Task AboutToShowAsync(int Id) - { - return false; - } - - public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) - { - return (Array.Empty(), Array.Empty()); - } + switch (context.Request.InterfaceAsString) + { + case "com.canonical.dbusmenu": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("GetLayout", "iias"): + { + using var writer = context.CreateReplyWriter("u(ia{sv}av)"); + var reader = context.Request.GetBodyReader(); + var parentId = reader.ReadInt32(); + var recursionDepth = reader.ReadInt32(); + var propertyNames = reader.ReadArray(); + var menu = GetMenu(parentId); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + writer.WriteUInt32(_revision); + writer.WriteStruct(layout); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); + } + + context.Reply(writer.CreateMessage()); + break; + } + case ("GetGroupProperties", "aias"): + { + using var writer = context.CreateReplyWriter("a(ia{sv})"); + var reader = context.Request.GetBodyReader(); + var ids = reader.ReadArray(); + var propertyNames = reader.ReadArray(); + var arrayStart = writer.WriteArrayStart(DBusType.Struct); + foreach (var id in ids) + { + var item = GetMenu(id); + var props = GetProperties(item, propertyNames); + writer.WriteStruct((id, props)); + } + + writer.WriteArrayEnd(arrayStart); + context.Reply(writer.CreateMessage()); + break; + } + case ("GetProperty", "is"): + { + using var writer = context.CreateReplyWriter("v"); + var reader = context.Request.GetBodyReader(); + var id = reader.ReadInt32(); + var name = reader.ReadString(); + writer.WriteVariant(GetProperty(GetMenu(id), name) ?? 0); + context.Reply(writer.CreateMessage()); + break; + } + case ("Event", "isvu"): + { + var reader = context.Request.GetBodyReader(); + var id = reader.ReadInt32(); + var eventId = reader.ReadString(); + var data = reader.ReadVariant(); + var timestamp = reader.ReadUInt32(); + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); + break; + } + case ("EventGroup", "a(isvu)"): + { + using var writer = context.CreateReplyWriter("ai"); + var reader = context.Request.GetBodyReader(); + var events = reader.ReadArray<(int Id, string EventId, object Data, uint Timestamp)>(); + foreach (var e in events) + Dispatcher.UIThread.Post(() => HandleEvent(e.Id, e.EventId, e.Data, e.Timestamp)); + writer.WriteArray(Array.Empty()); + context.Reply(writer.CreateMessage()); + break; + } + case ("AboutToShow", "i"): + { + using var writer = context.CreateReplyWriter("b"); + writer.WriteBool(false); + context.Reply(writer.CreateMessage()); + break; + } + case ("AboutToShowGroup", "ai"): + { + using var writer = context.CreateReplyWriter("aiai"); + writer.WriteStruct((Array.Empty(), Array.Empty())); + context.Reply(writer.CreateMessage()); + break; + } + } - #region Events + break; + case "org.freedesktop.DBus.Properties": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("Version", "u"): + { + using var writer = context.CreateReplyWriter("u"); + writer.WriteUInt32(_dBusMenuProperties.Version); + context.Reply(writer.CreateMessage()); + break; + } + case ("TextDirection", "s"): + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_dBusMenuProperties.TextDirection); + context.Reply(writer.CreateMessage()); + break; + } + case ("Status", "s"): + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_dBusMenuProperties.Status); + context.Reply(writer.CreateMessage()); + break; + } + case ("IconThemePath", "as"): + { + using var writer = context.CreateReplyWriter("as"); + writer.WriteArray(_dBusMenuProperties.IconThemePath); + context.Reply(writer.CreateMessage()); + break; + } + } - private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> - ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)>? LayoutUpdated; - private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } - private event Action PropertiesChanged { add { } remove { } } + break; + } - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) - { - ItemsPropertiesUpdated += handler; - return Disposable.Create(() => ItemsPropertiesUpdated -= handler); - } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) - { - LayoutUpdated += handler; - return Disposable.Create(() => LayoutUpdated -= handler); + return default; } - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) - { - ItemActivationRequested+= handler; - return Disposable.Create(() => ItemActivationRequested -= handler); - } + public bool RunMethodHandlerSynchronously(Message message) => true; - async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) + private void EmitUIntIntSignal(string member, uint arg0, int arg1) { - PropertiesChanged += handler; - return Disposable.Create(() => PropertiesChanged -= handler); + using var writer = _connection.GetMessageWriter(); + writer.WriteSignalHeader(null, Path, "com.canonical.dbusmenu", member, "ui"); + writer.WriteUInt32(arg0); + writer.WriteInt32(arg1); + _connection.TrySendMessage(writer.CreateMessage()); } - - #endregion } } } diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs deleted file mode 100644 index d84905324f..0000000000 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.Request")] - internal interface IRequest : IDBusObject - { - Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 8597f3922a..d59af3b5dd 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -4,45 +4,33 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; - -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { internal class DBusSystemDialog : BclStorageProvider { - private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")); - - internal static async Task TryCreate(IPlatformHandle handle) + internal static async Task TryCreateAsync(IPlatformHandle handle) { - if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser) - { - try - { - await fileChooser.GetVersionAsync(); - return new DBusSystemDialog(fileChooser, handle); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); - return null; - } - } - - return null; + if (DBusHelper.Connection is null) + return null; + var services = await DBusHelper.Connection.ListServicesAsync(); + if (!services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal)) + return null; + return new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle); } - private readonly IFileChooser _fileChooser; + private readonly DesktopService _desktopService; + private readonly FileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle) + private DBusSystemDialog(DesktopService desktopService, IPlatformHandle handle) { - _fileChooser = fileChooser; + _desktopService = desktopService; + _fileChooser = desktopService.CreateFileChooser("/org/freedesktop/portal/desktop"); _handle = handle; } @@ -59,20 +47,24 @@ namespace Avalonia.FreeDesktop var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeFilter); if (filters.Any()) - { chooserOptions.Add("filters", filters); - } chooserOptions.Add("multiple", options.AllowMultiple); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = _desktopService.CreateRequest(objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + tsc.TrySetException(e); + else + tsc.TrySetResult(x.results["uris"] as string[]); + }); - return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); + var uris = await tsc.Task ?? Array.Empty(); + return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); } public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) @@ -82,33 +74,33 @@ namespace Avalonia.FreeDesktop var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeChoices); if (filters.Any()) - { chooserOptions.Add("filters", filters); - } if (options.SuggestedFileName is { } currentName) chooserOptions.Add("current_name", currentName); if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true) chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); - objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); + var request = _desktopService.CreateRequest(objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + tsc.TrySetException(e); + else + tsc.TrySetResult(x.results["uris"] as string[]); + }); + var uris = await tsc.Task; var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null; if (path is null) - { return null; - } - else - { - // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. - path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); - return new BclStorageFile(new FileInfo(path)); - } + // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. + path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); + return new BclStorageFile(new FileInfo(path)); } public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) @@ -119,27 +111,31 @@ namespace Avalonia.FreeDesktop { "directory", true }, { "multiple", options.AllowMultiple } }; + var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = _desktopService.CreateRequest(objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + tsc.TrySetException(e); + else + tsc.TrySetResult(x.results["uris"] as string[]); + }); + var uris = await tsc.Task ?? Array.Empty(); return uris - .Select(path => new Uri(path).LocalPath) + .Select(static path => new Uri(path).LocalPath) // WSL2 freedesktop allows to select files as well in directory picker, filter it out. .Where(Directory.Exists) - .Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); + .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList? fileTypes) { // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] - if (fileTypes is null) - { return Array.Empty<(string name, (uint style, string extension)[])>(); - } var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) @@ -150,18 +146,11 @@ namespace Avalonia.FreeDesktop var extensions = Enumerable.Empty<(uint, string)>(); if (fileType.Patterns is { } patterns) - { extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); - } else if (fileType.MimeTypes is { } mimeTypes) - { extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); - } - if (extensions.Any()) - { filters.Add((fileType.Name, extensions.ToArray())); - } } return filters.ToArray(); diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index e37067d05c..ed041cb2ca 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,16 +1,12 @@ -#nullable enable - -using System; +using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { @@ -20,11 +16,12 @@ namespace Avalonia.FreeDesktop private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private IDisposable? _serviceWatchDisposable; + private readonly DBus? _dBus; + private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private IStatusNotifierWatcher? _statusNotifierWatcher; - private DbusPixmap _icon; + private StatusNotifierWatcher? _statusNotifierWatcher; + private (int, int, byte[]) _icon; private string? _sysTrayServiceName; private string? _tooltipText; @@ -51,6 +48,7 @@ namespace Avalonia.FreeDesktop IsActive = true; + _dBus = new DBusService(_connection, "org.freedesktop.DBus").CreateDBus("/org/freedesktop/DBus"); _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); @@ -60,47 +58,31 @@ namespace Avalonia.FreeDesktop private void InitializeSNWService() { - if (_connection is null || _isDisposed) return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - } - catch - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it."); - + if (_connection is null || _isDisposed) return; - } + _statusNotifierWatcher = new StatusNotifierWatcherService(_connection, "org.kde.StatusNotifierWatcher") + .CreateStatusNotifierWatcher("/StatusNotifierWatcher"); _serviceConnected = true; } private async void WatchAsync() { - try - { - _serviceWatchDisposable = - await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}"); - } + var services = await _connection!.ListServicesAsync(); + if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) + return; + + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((e, x) => { OnNameChange(x.A2); }); + var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); + OnNameChange(nameOwner); } - private void OnNameChange(ServiceOwnerChangedEventArgs obj) + private void OnNameChange(string? newOwner) { if (_isDisposed) return; - if (!_serviceConnected & obj.NewOwner != null) + if (!_serviceConnected & newOwner is not null) { _serviceConnected = true; InitializeSNWService(); @@ -108,55 +90,45 @@ namespace Avalonia.FreeDesktop DestroyTrayIcon(); if (_isVisible) - { CreateTrayIcon(); - } } - else if (_serviceConnected & obj.NewOwner is null) + else if (_serviceConnected & newOwner is null) { DestroyTrayIcon(); _serviceConnected = false; } } - private void CreateTrayIcon() + private async void CreateTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) return; +#if NET5_0_OR_GREATER + var pid = Environment.ProcessId; +#else var pid = Process.GetCurrentProcess().Id; +#endif var tid = s_trayIconInstanceId++; _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - - try - { - _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - _connection.RegisterServiceAsync(_sysTrayServiceName); - _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, $"Error creating a DBus tray icon: {e}."); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath); - _serviceConnected = false; - } + _connection.AddMethodHandler(_statusNotifierItemDbusObj); + await _dBus!.RequestNameAsync(_sysTrayServiceName, 0); + await _statusNotifierWatcher!.RegisterStatusNotifierItemAsync(_sysTrayServiceName); _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; } private void DestroyTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - _connection.UnregisterServiceAsync(_sysTrayServiceName); + _dBus.ReleaseNameAsync(_sysTrayServiceName); } public void Dispose() @@ -175,13 +147,14 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); + _statusNotifierItemDbusObj?.SetIcon((1, 1, new byte[] { 255, 0, 0, 0 })); return; } var x11iconData = IconConverterDelegate(icon); - if (x11iconData.Length == 0) return; + if (x11iconData.Length == 0) + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -199,7 +172,7 @@ namespace Avalonia.FreeDesktop pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); } - _icon = new DbusPixmap(w, h, pixByteArray); + _icon = (w, h, pixByteArray); _statusNotifierItemDbusObj?.SetIcon(_icon); } @@ -237,111 +210,38 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem + internal class StatusNotifierItemDbusObj : IMethodHandler { + private readonly Connection _connection; private readonly StatusNotifierItemProperties _backingProperties; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) - { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) + { + _connection = connection; _backingProperties = new StatusNotifierItemProperties { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") + Menu = dbusMenuPath, // Needs a dbus menu somehow + ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty) }; InvalidateAll(); } - public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; - - public Task ActivateAsync(int x, int y) - { - ActivationDelegate?.Invoke(); - return Task.CompletedTask; - } + public string Path => "/StatusNotifierItem"; - public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - - public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + public event Action? ActivationDelegate; public void InvalidateAll() { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } - - public Task WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } - - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } - - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); + EmitVoidSignal("NewTitle"); + EmitVoidSignal("NewIcon"); + EmitVoidSignal("NewAttentionIcon"); + EmitVoidSignal("NewOverlayIcon"); + EmitVoidSignal("NewToolTip"); + EmitStringSignal("NewStatus", _backingProperties.Status ?? string.Empty); } - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } - - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) - { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); - } - - public Task GetAsync(string prop) - { - return Task.FromResult(prop switch - { - nameof(_backingProperties.Category) => _backingProperties.Category, - nameof(_backingProperties.Id) => _backingProperties.Id, - nameof(_backingProperties.Menu) => _backingProperties.Menu, - nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, - nameof(_backingProperties.Status) => _backingProperties.Status, - nameof(_backingProperties.Title) => _backingProperties.Title, - nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, - _ => null - }); - } - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action handler) => - Task.FromResult(Disposable.Empty); - - public void SetIcon(DbusPixmap dbusPixmap) + public void SetIcon((int, int, byte[]) dbusPixmap) { _backingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); @@ -356,98 +256,153 @@ namespace Avalonia.FreeDesktop _backingProperties.Category = "ApplicationStatus"; _backingProperties.Status = text; _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - + _backingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } - } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - [DBusInterface("org.kde.StatusNotifierItem")] - internal interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int x, int y); - Task ActivateAsync(int x, int y); - Task SecondaryActivateAsync(int x, int y); - Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - // This class is used by Tmds.Dbus to ferry properties - // from the SNI spec. - // Don't change this to actual C# properties since - // Tmds.Dbus will get confused. - [Dictionary] - internal class StatusNotifierItemProperties - { - public string? Category; - - public string? Id; - - public string? Title; + public bool RunMethodHandlerSynchronously(Message message) => false; - public string? Status; - - public ObjectPath Menu; - - public DbusPixmap[]? IconPixmap; + public ValueTask HandleMethodAsync(MethodContext context) + { + switch (context.Request.InterfaceAsString) + { + case "org.kde.StatusNotifierItem": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("ContextMenu", "ii"): + break; + case ("Activate", "ii"): + ActivationDelegate?.Invoke(); + break; + case ("SecondaryActivate", "ii"): + break; + case ("Scroll", "is"): + break; + } - public ToolTip ToolTip; - } + break; + case "org.freedesktop.DBus.Properties": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("Get", "ss"): + { + var reader = context.Request.GetBodyReader(); + var interfaceName = reader.ReadString(); + var member = reader.ReadString(); + switch (member) + { + case "Category": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Category); + context.Reply(writer.CreateMessage()); + break; + } + case "Id": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Id); + context.Reply(writer.CreateMessage()); + break; + } + case "Title": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Title); + context.Reply(writer.CreateMessage()); + break; + } + case "Status": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Status); + context.Reply(writer.CreateMessage()); + break; + } + case "Menu": + { + using var writer = context.CreateReplyWriter("o"); + writer.WriteObjectPath(_backingProperties.Menu); + context.Reply(writer.CreateMessage()); + break; + } + case "IconPixmap": + { + using var writer = context.CreateReplyWriter("a(iiay)"); + writer.WriteArray(_backingProperties.IconPixmap); + context.Reply(writer.CreateMessage()); + break; + } + case "ToolTip": + { + using var writer = context.CreateReplyWriter("(sa(iiay)ss)"); + writer.WriteStruct(_backingProperties.ToolTip); + context.Reply(writer.CreateMessage()); + break; + } + } + + break; + } + case ("GetAll", "s"): + { + var writer = context.CreateReplyWriter("a{sv}"); + var dict = new Dictionary + { + { "Category", _backingProperties.Category ?? string.Empty }, + { "Id", _backingProperties.Id ?? string.Empty }, + { "Title", _backingProperties.Title ?? string.Empty }, + { "Status", _backingProperties.Status ?? string.Empty }, + { "Menu", _backingProperties.Menu }, + { "IconPixmap", _backingProperties.IconPixmap }, + { "ToolTip", _backingProperties.ToolTip } + }; + + writer.WriteDictionary(dict); + context.Reply(writer.CreateMessage()); + break; + } + } - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; + break; + } - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) - }; + return default; + } - public ToolTip(string message) : this("", s_blank, message, "") + private void EmitVoidSignal(string member) { + using var writer = _connection.GetMessageWriter(); + writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member); + _connection.TrySendMessage(writer.CreateMessage()); } - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + private void EmitStringSignal(string member, string value) { - First = first; - Second = second; - Third = third; - Fourth = fourth; + using var writer = _connection.GetMessageWriter(); + writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member, "s"); + writer.WriteString(value); + _connection.TrySendMessage(writer.CreateMessage()); } - } - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - - public DbusPixmap(int width, int height, byte[] data) + private record StatusNotifierItemProperties { - Width = width; - Height = height; - Data = data; + public string? Category { get; set; } + public string? Id { get; set; } + public string? Title { get; set; } + public string? Status { get; set; } + public int WindowId { get; set; } + public string? IconThemePath { get; set; } + public ObjectPath Menu { get; set; } + public bool ItemIsMenu { get; set; } + public string? IconName { get; set; } + public (int, int, byte[])[]? IconPixmap { get; set; } + public string? OverlayIconName { get; set; } + public (int, int, byte[])[]? OverlayIconPixmap { get; set; } + public string? AttentionIconName { get; set; } + public (int, int, byte[])[]? AttentionIconPixmap { get; set; } + public string? AttentionMovieName { get; set; } + public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } } - - public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 }); } } diff --git a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs new file mode 100644 index 0000000000..a0ca8abc97 --- /dev/null +++ b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs @@ -0,0 +1,937 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop +{ + internal record NotificationProperties + { + public uint Version { get; set; } + } + + internal class Notification : DesktopObject + { + private const string Interface = "org.freedesktop.portal.Notification"; + + public Notification(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task AddNotificationAsync(string id, Dictionary notification) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sa{sv}", + member: "AddNotification"); + writer.WriteString(id); + writer.WriteDictionary(notification); + return writer.CreateMessage(); + } + } + + public Task RemoveNotificationAsync(string id) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RemoveNotification"); + writer.WriteString(id); + return writer.CreateMessage(); + } + } + + public ValueTask WatchActionInvokedAsync(Action handler, + bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", + (m, s) => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static NotificationProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static NotificationProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new NotificationProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal record OpenURIProperties + { + public uint Version { get; set; } + } + + internal class OpenURI : DesktopObject + { + private const string Interface = "org.freedesktop.portal.OpenURI"; + + public OpenURI(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task OpenUriAsync(string parentWindow, string uri, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "OpenURI"); + writer.WriteString(parentWindow); + writer.WriteString(uri); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sha{sv}", + member: "OpenFile"); + writer.WriteString(parentWindow); + writer.WriteHandle(fd); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sha{sv}", + member: "OpenDirectory"); + writer.WriteString(parentWindow); + writer.WriteHandle(fd); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static OpenURIProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static OpenURIProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new OpenURIProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal record DynamicLauncherProperties + { + public uint SupportedLauncherTypes { get; set; } + public uint Version { get; set; } + } + + internal class DynamicLauncher : DesktopObject + { + private const string Interface = "org.freedesktop.portal.DynamicLauncher"; + + public DynamicLauncher(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task InstallAsync(string token, string desktopFileId, string desktopEntry, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sssa{sv}", + member: "Install"); + writer.WriteString(token); + writer.WriteString(desktopFileId); + writer.WriteString(desktopEntry); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssva{sv}", + member: "PrepareInstall"); + writer.WriteString(parentWindow); + writer.WriteString(name); + writer.WriteVariant(iconV); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task RequestInstallTokenAsync(string name, object iconV, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sva{sv}", + member: "RequestInstallToken"); + writer.WriteString(name); + writer.WriteVariant(iconV); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task UninstallAsync(string desktopFileId, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sa{sv}", + member: "Uninstall"); + writer.WriteString(desktopFileId); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task GetDesktopEntryAsync(string desktopFileId) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetDesktopEntry"); + writer.WriteString(desktopFileId); + return writer.CreateMessage(); + } + } + + public Task<(object IconV, string IconFormat, uint IconSize)> GetIconAsync(string desktopFileId) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_vsu(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetIcon"); + writer.WriteString(desktopFileId); + return writer.CreateMessage(); + } + } + + public Task LaunchAsync(string desktopFileId, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sa{sv}", + member: "Launch"); + writer.WriteString(desktopFileId); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SetSupportedLauncherTypesAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("SupportedLauncherTypes"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetSupportedLauncherTypesAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static DynamicLauncherProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "SupportedLauncherTypes": + invalidated.Add("SupportedLauncherTypes"); + break; + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static DynamicLauncherProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new DynamicLauncherProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "SupportedLauncherTypes": + reader.ReadSignature("u"); + props.SupportedLauncherTypes = reader.ReadUInt32(); + changedList?.Add("SupportedLauncherTypes"); + break; + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal record FileChooserProperties + { + public uint Version { get; set; } + } + + internal class FileChooser : DesktopObject + { + private const string Interface = "org.freedesktop.portal.FileChooser"; + + public FileChooser(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task OpenFileAsync(string parentWindow, string title, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "OpenFile"); + writer.WriteString(parentWindow); + writer.WriteString(title); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SaveFileAsync(string parentWindow, string title, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "SaveFile"); + writer.WriteString(parentWindow); + writer.WriteString(title); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SaveFilesAsync(string parentWindow, string title, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "SaveFiles"); + writer.WriteString(parentWindow); + writer.WriteString(title); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static FileChooserProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static FileChooserProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new FileChooserProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal class Request : DesktopObject + { + private const string Interface = "org.freedesktop.portal.Request"; + + public Request(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task CloseAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Close"); + return writer.CreateMessage(); + } + } + + public ValueTask WatchResponseAsync(Action results)> handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "Response", + static (m, s) => ReadMessage_uaesv(m, (DesktopObject)s!), handler, emitOnCapturedContext); + } + + internal class DesktopService + { + public DesktopService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public Notification CreateNotification(string path) => new(this, path); + public OpenURI CreateOpenUri(string path) => new(this, path); + public DynamicLauncher CreateDynamicLauncher(string path) => new(this, path); + public FileChooser CreateFileChooser(string path) => new(this, path); + public Request CreateRequest(string path) => new(this, path); + } + + internal class DesktopObject + { + protected DesktopObject(DesktopService service, ObjectPath path) + => (Service, Path) = (service, path); + + public DesktopService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, changes, rs, hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + protected static ObjectPath ReadMessage_o(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadObjectPath(); + } + + protected static uint ReadMessage_v_u(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("u"); + return reader.ReadUInt32(); + } + + protected static (string, string, object[]) ReadMessage_ssav(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadArray(); + return (arg0, arg1, arg2); + } + + protected static (uint, Dictionary) ReadMessage_uaesv(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadDictionary(); + return (arg0, arg1); + } + + protected static string ReadMessage_s(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static (object, string, uint) ReadMessage_vsu(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadUInt32(); + return (arg0, arg1, arg2); + } + } + + internal class PropertyChanges + { + public PropertyChanges(TProperties properties, string[] invalidated, string[] changed) + => (Properties, Invalidated, Changed) = (properties, invalidated, changed); + + public TProperties Properties { get; } + public string[] Invalidated { get; } + public string[] Changed { get; } + public bool HasChanged(string property) => Array.IndexOf(Changed, property) != -1; + public bool IsInvalidated(string property) => Array.IndexOf(Invalidated, property) != -1; + } +} diff --git a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs new file mode 100644 index 0000000000..5cd44905af --- /dev/null +++ b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop +{ + internal record StatusNotifierWatcherProperties + { + public string[] RegisteredStatusNotifierItems { get; set; } = default!; + public bool IsStatusNotifierHostRegistered { get; set; } + public int ProtocolVersion { get; set; } + } + + internal class StatusNotifierWatcher : StatusNotifierWatcherObject + { + private const string Interface = "org.kde.StatusNotifierWatcher"; + + public StatusNotifierWatcher(StatusNotifierWatcherService service, ObjectPath path) : base(service, path) { } + + public Task RegisterStatusNotifierItemAsync(string service) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RegisterStatusNotifierItem"); + writer.WriteString(service); + return writer.CreateMessage(); + } + } + + public Task RegisterStatusNotifierHostAsync(string service) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RegisterStatusNotifierHost"); + writer.WriteString(service); + return writer.CreateMessage(); + } + } + + public ValueTask WatchStatusNotifierItemRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemRegistered", static (m, s) => + ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchStatusNotifierItemUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemUnregistered", static (m, s) + => ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchStatusNotifierHostRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostRegistered", handler, emitOnCapturedContext); + + public ValueTask WatchStatusNotifierHostUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostUnregistered", handler, emitOnCapturedContext); + + public Task SetRegisteredStatusNotifierItemsAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("RegisteredStatusNotifierItems"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + + public Task SetIsStatusNotifierHostRegisteredAsync(bool value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("IsStatusNotifierHostRegistered"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + + public Task SetProtocolVersionAsync(int value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("ProtocolVersion"); + writer.WriteSignature("i"); + writer.WriteInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetRegisteredStatusNotifierItemsAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "RegisteredStatusNotifierItems"), static (m, s) + => ReadMessage_v_as(m, (StatusNotifierWatcherObject)s!), this); + + public Task GetIsStatusNotifierHostRegisteredAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IsStatusNotifierHostRegistered"), static (m, s) => + ReadMessage_v_b(m, (StatusNotifierWatcherObject)s!), this); + + public Task GetProtocolVersionAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "ProtocolVersion"), static (m, s) + => ReadMessage_v_i(m, (StatusNotifierWatcherObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) + => ReadMessage(m, (StatusNotifierWatcherObject)s!), this); + + static StatusNotifierWatcherProperties ReadMessage(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, static (m, s) => ReadMessage(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + var headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new List(); + var property = reader.ReadString(); + switch (property) + { + case "RegisteredStatusNotifierItems": + invalidated.Add("RegisteredStatusNotifierItems"); + break; + case "IsStatusNotifierHostRegistered": + invalidated.Add("IsStatusNotifierHostRegistered"); + break; + case "ProtocolVersion": + invalidated.Add("ProtocolVersion"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new StatusNotifierWatcherProperties(); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "RegisteredStatusNotifierItems": + reader.ReadSignature("as"); + props.RegisteredStatusNotifierItems = reader.ReadArray(); + changedList?.Add("RegisteredStatusNotifierItems"); + break; + case "IsStatusNotifierHostRegistered": + reader.ReadSignature("b"); + props.IsStatusNotifierHostRegistered = reader.ReadBool(); + changedList?.Add("IsStatusNotifierHostRegistered"); + break; + case "ProtocolVersion": + reader.ReadSignature("i"); + props.ProtocolVersion = reader.ReadInt32(); + changedList?.Add("ProtocolVersion"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal class StatusNotifierWatcherService + { + public StatusNotifierWatcherService(Connection connection, string destination) => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + + public StatusNotifierWatcher CreateStatusNotifierWatcher(string path) => new(this, path); + } + + internal class StatusNotifierWatcherObject + { + protected StatusNotifierWatcherObject(StatusNotifierWatcherService service, ObjectPath path) + { + (Service, Path) = (service, path); + } + + public StatusNotifierWatcherService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) + => null!, static (Exception? ex, object _, object? _, object? hs) + => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static string ReadMessage_s(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static string[] ReadMessage_v_as(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArray(); + } + + protected static bool ReadMessage_v_b(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("b"); + return reader.ReadBool(); + } + + protected static int ReadMessage_v_i(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("i"); + return reader.ReadInt32(); + } + } +} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 810a806c8a..e0d6cf3e55 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -212,10 +212,10 @@ namespace Avalonia.X11 _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1); } - StorageProvider = new CompositeStorageProvider(new Func>[] + StorageProvider = new CompositeStorageProvider(new[] { - () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult(null), - () => GtkSystemDialog.TryCreate(this), + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), + () => GtkSystemDialog.TryCreate(this) }); } diff --git a/src/Linux/Tmds.DBus b/src/Linux/Tmds.DBus new file mode 160000 index 0000000000..bfca94ab05 --- /dev/null +++ b/src/Linux/Tmds.DBus @@ -0,0 +1 @@ +Subproject commit bfca94ab052683c7ccef51e4a036098f539cc676 From d5df9298bd2642492654b56505180da89c4faf6e Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 30 Dec 2022 14:34:10 +0100 Subject: [PATCH 02/36] Fix warnings --- .../AppMenuRegistrar.DBus.cs | 25 ++- src/Avalonia.FreeDesktop/DBus.DBus.cs | 14 +- src/Avalonia.FreeDesktop/DBusCallQueue.cs | 13 +- .../DBusIme/Fcitx/FcitxEnums.cs | 64 ++++---- .../DBusIme/IBus/IBusEnums.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenu.DBus.cs | 131 ++++++++++------ src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 3 +- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 18 +-- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 32 ++-- .../FreeDesktopPortalDesktop.DBus.cs | 143 +++++++++--------- src/Avalonia.FreeDesktop/IX11InputMethod.cs | 4 +- src/Avalonia.FreeDesktop/NativeMethods.cs | 8 +- .../StatusNotifierWatcher.DBus.cs | 25 ++- 13 files changed, 258 insertions(+), 224 deletions(-) diff --git a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs index a45d1642fc..75c8ce61a5 100644 --- a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs +++ b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs @@ -1,9 +1,7 @@ using System; using System.Threading.Tasks; - using Tmds.DBus.Protocol; - namespace Avalonia.FreeDesktop { internal class Registrar : AppMenuRegistrarObject @@ -52,7 +50,7 @@ namespace Avalonia.FreeDesktop public Task<(string Service, ObjectPath MenuObjectPath)> GetMenuForWindowAsync(uint windowId) { - return Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); MessageBuffer CreateMessage() { @@ -76,7 +74,7 @@ namespace Avalonia.FreeDesktop public Connection Connection { get; } public string Destination { get; } - public Registrar CreateRegistrar(string path) => new Registrar(this, path); + public Registrar CreateRegistrar(string path) => new(this, path); } internal class AppMenuRegistrarObject @@ -84,8 +82,8 @@ namespace Avalonia.FreeDesktop protected AppMenuRegistrarObject(RegistrarService service, ObjectPath path) => (Service, Path) = (service, path); - public RegistrarService Service { get; } - public ObjectPath Path { get; } + protected RegistrarService Service { get; } + protected ObjectPath Path { get; } protected Connection Connection => Service.Connection; protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) @@ -128,9 +126,8 @@ namespace Avalonia.FreeDesktop Member = "PropertiesChanged", Arg0 = @interface }; - return Connection.AddMatchAsync(rule, reader, - (Exception? ex, PropertyChanges changes, object? rs, object? hs) => - ((Action>)hs!).Invoke(ex, changes), + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) + => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } @@ -145,9 +142,8 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (Exception? ex, TArg arg, object? rs, object? hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, @@ -161,8 +157,9 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, (Message message, object? state) => null!, - (Exception? ex, object v, object? rs, object? hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, static (_, _) + => null!, static (ex, _, _, hs) + => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); } protected static (string, ObjectPath) ReadMessage_so(Message message, AppMenuRegistrarObject _) diff --git a/src/Avalonia.FreeDesktop/DBus.DBus.cs b/src/Avalonia.FreeDesktop/DBus.DBus.cs index 4cf2b76b45..0f83ddaa4a 100644 --- a/src/Avalonia.FreeDesktop/DBus.DBus.cs +++ b/src/Avalonia.FreeDesktop/DBus.DBus.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; - using Tmds.DBus.Protocol; - namespace Avalonia.FreeDesktop { internal record DBusProperties @@ -417,10 +415,10 @@ namespace Avalonia.FreeDesktop public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) => - ReadMessage(m, (DBusObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => + ReadMessage(m), this); - static DBusProperties ReadMessage(Message message, DBusObject _) + static DBusProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -429,10 +427,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, static (m, s) => - ReadMessage(m, (DBusObject)s!), handler, emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) => + ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DBusObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index e7c07dcbf9..b853626d45 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -7,19 +7,20 @@ namespace Avalonia.FreeDesktop class DBusCallQueue { private readonly Func _errorHandler; + private readonly Queue _q = new(); - record Item(Func Callback) + private bool _processing; + + private record Item(Func Callback) { public Action? OnFinish; } - private Queue _q = new Queue(); - private bool _processing; public DBusCallQueue(Func errorHandler) { _errorHandler = errorHandler; } - + public void Enqueue(Func cb) { _q.Enqueue(new Item(cb)); @@ -42,7 +43,7 @@ namespace Avalonia.FreeDesktop Process(); return tcs.Task; } - + public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -62,7 +63,7 @@ namespace Avalonia.FreeDesktop return tcs.Task; } - async void Process() + private async void Process() { if(_processing) return; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs index 6510a5877a..fafc66b6b9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs @@ -2,45 +2,45 @@ using System; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { - enum FcitxKeyEventType + internal enum FcitxKeyEventType { FCITX_PRESS_KEY, FCITX_RELEASE_KEY - }; - + } + [Flags] - enum FcitxCapabilityFlags + internal enum FcitxCapabilityFlags { CAPACITY_NONE = 0, - CAPACITY_CLIENT_SIDE_UI = (1 << 0), - CAPACITY_PREEDIT = (1 << 1), - CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), - CAPACITY_PASSWORD = (1 << 3), - CAPACITY_FORMATTED_PREEDIT = (1 << 4), - CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), - CAPACITY_SURROUNDING_TEXT = (1 << 6), - CAPACITY_EMAIL = (1 << 7), - CAPACITY_DIGIT = (1 << 8), - CAPACITY_UPPERCASE = (1 << 9), - CAPACITY_LOWERCASE = (1 << 10), - CAPACITY_NOAUTOUPPERCASE = (1 << 11), - CAPACITY_URL = (1 << 12), - CAPACITY_DIALABLE = (1 << 13), - CAPACITY_NUMBER = (1 << 14), - CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), - CAPACITY_SPELLCHECK = (1 << 16), - CAPACITY_NO_SPELLCHECK = (1 << 17), - CAPACITY_WORD_COMPLETION = (1 << 18), - CAPACITY_UPPERCASE_WORDS = (1 << 19), - CAPACITY_UPPERCASE_SENTENCES = (1 << 20), - CAPACITY_ALPHA = (1 << 21), - CAPACITY_NAME = (1 << 22), - CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), - CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), - }; + CAPACITY_CLIENT_SIDE_UI = 1 << 0, + CAPACITY_PREEDIT = 1 << 1, + CAPACITY_CLIENT_SIDE_CONTROL_STATE = 1 << 2, + CAPACITY_PASSWORD = 1 << 3, + CAPACITY_FORMATTED_PREEDIT = 1 << 4, + CAPACITY_CLIENT_UNFOCUS_COMMIT = 1 << 5, + CAPACITY_SURROUNDING_TEXT = 1 << 6, + CAPACITY_EMAIL = 1 << 7, + CAPACITY_DIGIT = 1 << 8, + CAPACITY_UPPERCASE = 1 << 9, + CAPACITY_LOWERCASE = 1 << 10, + CAPACITY_NOAUTOUPPERCASE = 1 << 11, + CAPACITY_URL = 1 << 12, + CAPACITY_DIALABLE = 1 << 13, + CAPACITY_NUMBER = 1 << 14, + CAPACITY_NO_ON_SCREEN_KEYBOARD = 1 << 15, + CAPACITY_SPELLCHECK = 1 << 16, + CAPACITY_NO_SPELLCHECK = 1 << 17, + CAPACITY_WORD_COMPLETION = 1 << 18, + CAPACITY_UPPERCASE_WORDS = 1 << 19, + CAPACITY_UPPERCASE_SENTENCES = 1 << 20, + CAPACITY_ALPHA = 1 << 21, + CAPACITY_NAME = 1 << 22, + CAPACITY_GET_IM_INFO_ON_FOCUS = 1 << 23, + CAPACITY_RELATIVE_CURSOR_RECT = 1 << 24 + } [Flags] - enum FcitxKeyState + internal enum FcitxKeyState { FcitxKeyState_None = 0, FcitxKeyState_Shift = 1 << 0, @@ -63,5 +63,5 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx FcitxKeyState_Hyper = 1 << 27, FcitxKeyState_Meta = 1 << 28, FcitxKeyState_UsedMask = 0x5c001fff - }; + } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs index b8ac816694..1b430f9c90 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -40,6 +40,6 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus CapLookupTable = 1 << 2, CapFocus = 1 << 3, CapProperty = 1 << 4, - CapSurroundingText = 1 << 5, + CapSurroundingText = 1 << 5 } } diff --git a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs index d9016d8644..e3928cfbb2 100644 --- a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs +++ b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs @@ -16,11 +16,12 @@ namespace Avalonia.FreeDesktop internal class DBusMenu : DBusMenuObject { private const string Interface = "com.canonical.dbusmenu"; - public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) - { } + + public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) { } + public Task<(uint Revision, (int, Dictionary, object[]) Layout)> GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_uriaesvavz(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_uriaesvavz(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -36,9 +37,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task<(int, Dictionary)[]> GetGroupPropertiesAsync(int[] ids, string[] propertyNames) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ariaesvz(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ariaesvz(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -53,9 +55,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task GetPropertyAsync(int id, string name) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_v(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_v(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -70,6 +73,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task EventAsync(int id, string eventId, object data, uint timestamp) { return Connection.CallMethodAsync(CreateMessage()); @@ -89,9 +93,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task EventGroupAsync((int, string, object, uint)[] events) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ai(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ai(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -105,9 +110,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task AboutToShowAsync(int id) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_b(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_b(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -121,9 +127,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task<(int[] UpdatesNeeded, int[] IdErrors)> AboutToShowGroupAsync(int[] ids) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_aiai(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_aiai(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -137,12 +144,16 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public ValueTask WatchItemsPropertiesUpdatedAsync(Action)[] UpdatedProps, (int, string[])[] RemovedProps)> handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", (m, s) => ReadMessage_ariaesvzariasz(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", static (m, _) => ReadMessage_ariaesvzariasz(m), handler, emitOnCapturedContext); + public ValueTask WatchLayoutUpdatedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", (m, s) => ReadMessage_ui(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", static (m, _) => ReadMessage_ui(m), handler, emitOnCapturedContext); + public ValueTask WatchItemActivationRequestedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", (m, s) => ReadMessage_iu(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", static (m, _) => ReadMessage_iu(m), handler, emitOnCapturedContext); + public Task SetVersionAsync(uint value) { return Connection.CallMethodAsync(CreateMessage()); @@ -162,6 +173,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task SetTextDirectionAsync(string value) { return Connection.CallMethodAsync(CreateMessage()); @@ -181,6 +193,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task SetStatusAsync(string value) { return Connection.CallMethodAsync(CreateMessage()); @@ -200,6 +213,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task SetIconThemePathAsync(string[] value) { return Connection.CallMethodAsync(CreateMessage()); @@ -219,40 +233,46 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), (m, s) => ReadMessage_v_u(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), static (m, _) => ReadMessage_v_u(m), this); + public Task GetTextDirectionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), static (m, _) => ReadMessage_v_s(m), this); + public Task GetStatusAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), static (m, _) => ReadMessage_v_s(m), this); + public Task GetIconThemePathAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), (m, s) => ReadMessage_v_as(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), static (m, _) => ReadMessage_v_as(m), this); + public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), (m, s) => ReadMessage(m, (DBusMenuObject)s!), this); - static DBusMenuProperties ReadMessage(Message message, DBusMenuObject _) + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => ReadMessage(m), this); + static DBusMenuProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); } } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DBusMenuObject _) + return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface - List changed = new(), invalidated = new(); + List changed = new(); return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); } static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -265,10 +285,11 @@ namespace Avalonia.FreeDesktop return invalidated?.ToArray() ?? Array.Empty(); } } - private static DBusMenuProperties ReadProperties(ref Reader reader, List? changedList = null) + + private static DBusMenuProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new DBusMenuProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -309,7 +330,7 @@ namespace Avalonia.FreeDesktop public string Destination { get; } public DBusMenuService(Connection connection, string destination) => (Connection, Destination) = (connection, destination); - public DBusMenu CreateDbusmenu(string path) => new DBusMenu(this, path); + public DBusMenu CreateDbusmenu(string path) => new(this, path); } internal class DBusMenuObject @@ -319,6 +340,7 @@ namespace Avalonia.FreeDesktop protected Connection Connection => Service.Connection; protected DBusMenuObject(DBusMenuService service, ObjectPath path) => (Service, Path) = (service, path); + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) { using var writer = Connection.GetMessageWriter(); @@ -332,6 +354,7 @@ namespace Avalonia.FreeDesktop writer.WriteString(property); return writer.CreateMessage(); } + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) { using var writer = Connection.GetMessageWriter(); @@ -344,6 +367,7 @@ namespace Avalonia.FreeDesktop writer.WriteString(@interface); return writer.CreateMessage(); } + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) { var rule = new MatchRule @@ -355,11 +379,11 @@ namespace Avalonia.FreeDesktop Member = "PropertiesChanged", Arg0 = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, changes, rs, hs) => ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) + => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { @@ -369,11 +393,11 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) + => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { @@ -383,77 +407,90 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, (message, state) => null!, - (ex, v, rs, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, static (_, _) + => null!, static (ex, _, _, hs) + => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); } - protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message, DBusMenuObject _) + + protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadUInt32(); var arg1 = reader.ReadStruct, object[]>(); return (arg0, arg1); } - protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message, DBusMenuObject _) + + protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message) { var reader = message.GetBodyReader(); return reader.ReadArray<(int, Dictionary)>(); } - protected static object ReadMessage_v(Message message, DBusMenuObject _) + + protected static object ReadMessage_v(Message message) { var reader = message.GetBodyReader(); return reader.ReadVariant(); } - protected static int[] ReadMessage_ai(Message message, DBusMenuObject _) + + protected static int[] ReadMessage_ai(Message message) { var reader = message.GetBodyReader(); return reader.ReadArray(); } - protected static bool ReadMessage_b(Message message, DBusMenuObject _) + + protected static bool ReadMessage_b(Message message) { var reader = message.GetBodyReader(); return reader.ReadBool(); } - protected static (int[], int[]) ReadMessage_aiai(Message message, DBusMenuObject _) + + protected static (int[], int[]) ReadMessage_aiai(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadArray(); var arg1 = reader.ReadArray(); return (arg0, arg1); } - protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message, DBusMenuObject _) + + protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadArray<(int, Dictionary)>(); var arg1 = reader.ReadArray<(int, string[])>(); return (arg0, arg1); } - protected static (uint, int) ReadMessage_ui(Message message, DBusMenuObject _) + + protected static (uint, int) ReadMessage_ui(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadUInt32(); var arg1 = reader.ReadInt32(); return (arg0, arg1); } - protected static (int, uint) ReadMessage_iu(Message message, DBusMenuObject _) + + protected static (int, uint) ReadMessage_iu(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadInt32(); var arg1 = reader.ReadUInt32(); return (arg0, arg1); } - protected static uint ReadMessage_v_u(Message message, DBusMenuObject _) + + protected static uint ReadMessage_v_u(Message message) { var reader = message.GetBodyReader(); reader.ReadSignature("u"); return reader.ReadUInt32(); } - protected static string ReadMessage_v_s(Message message, DBusMenuObject _) + + protected static string ReadMessage_v_s(Message message) { var reader = message.GetBodyReader(); reader.ReadSignature("s"); return reader.ReadString(); } - protected static string[] ReadMessage_v_as(Message message, DBusMenuObject _) + + protected static string[] ReadMessage_v_as(Message message) { var reader = message.GetBodyReader(); reader.ReadSignature("as"); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 682434bc8d..69e2eed72f 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -96,8 +96,7 @@ namespace Avalonia.FreeDesktop public void SetNativeMenu(NativeMenu? menu) { - if (menu is null) - menu = new NativeMenu(); + menu ??= new NativeMenu(); if (_menu is not null) ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index d59af3b5dd..7d1a0bbfc4 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -18,9 +18,9 @@ namespace Avalonia.FreeDesktop if (DBusHelper.Connection is null) return null; var services = await DBusHelper.Connection.ListServicesAsync(); - if (!services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal)) - return null; - return new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle); + return services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal) + ? new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle) + : null; } private readonly DesktopService _desktopService; @@ -140,15 +140,15 @@ namespace Avalonia.FreeDesktop var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) { - const uint globStyle = 0u; - const uint mimeStyle = 1u; + const uint GlobStyle = 0u; + const uint MimeStyle = 1u; var extensions = Enumerable.Empty<(uint, string)>(); - if (fileType.Patterns is { } patterns) - extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); - else if (fileType.MimeTypes is { } mimeTypes) - extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); + if (fileType.Patterns is not null) + extensions = extensions.Concat(fileType.Patterns.Select(static x => (globStyle: GlobStyle, x))); + else if (fileType.MimeTypes is not null) + extensions = extensions.Concat(fileType.MimeTypes.Select(static x => (mimeStyle: MimeStyle, x))); if (extensions.Any()) filters.Add((fileType.Name, extensions.ToArray())); } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index ed041cb2ca..d6518f7246 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -72,7 +72,7 @@ namespace Avalonia.FreeDesktop if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) return; - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((e, x) => { OnNameChange(x.A2); }); + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.A2) ); var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); OnNameChange(nameOwner); } @@ -128,7 +128,7 @@ namespace Avalonia.FreeDesktop if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _dBus.ReleaseNameAsync(_sysTrayServiceName); + _dBus!.ReleaseNameAsync(_sysTrayServiceName); } public void Dispose() @@ -147,7 +147,7 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon((1, 1, new byte[] { 255, 0, 0, 0 })); + _statusNotifierItemDbusObj?.SetIcon(StatusNotifierItemDbusObj.StatusNotifierItemProperties.EmptyPixmap); return; } @@ -238,7 +238,7 @@ namespace Avalonia.FreeDesktop EmitVoidSignal("NewAttentionIcon"); EmitVoidSignal("NewOverlayIcon"); EmitVoidSignal("NewToolTip"); - EmitStringSignal("NewStatus", _backingProperties.Status ?? string.Empty); + EmitStringSignal("NewStatus", _backingProperties.Status); } public void SetIcon((int, int, byte[]) dbusPixmap) @@ -287,7 +287,7 @@ namespace Avalonia.FreeDesktop case ("Get", "ss"): { var reader = context.Request.GetBodyReader(); - var interfaceName = reader.ReadString(); + _ = reader.ReadString(); var member = reader.ReadString(); switch (member) { @@ -349,10 +349,10 @@ namespace Avalonia.FreeDesktop var writer = context.CreateReplyWriter("a{sv}"); var dict = new Dictionary { - { "Category", _backingProperties.Category ?? string.Empty }, - { "Id", _backingProperties.Id ?? string.Empty }, - { "Title", _backingProperties.Title ?? string.Empty }, - { "Status", _backingProperties.Status ?? string.Empty }, + { "Category", _backingProperties.Category }, + { "Id", _backingProperties.Id }, + { "Title", _backingProperties.Title }, + { "Status", _backingProperties.Status }, { "Menu", _backingProperties.Menu }, { "IconPixmap", _backingProperties.IconPixmap }, { "ToolTip", _backingProperties.ToolTip } @@ -385,24 +385,26 @@ namespace Avalonia.FreeDesktop _connection.TrySendMessage(writer.CreateMessage()); } - private record StatusNotifierItemProperties + internal record StatusNotifierItemProperties { - public string? Category { get; set; } - public string? Id { get; set; } - public string? Title { get; set; } - public string? Status { get; set; } + public string Category { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; public int WindowId { get; set; } public string? IconThemePath { get; set; } public ObjectPath Menu { get; set; } public bool ItemIsMenu { get; set; } public string? IconName { get; set; } - public (int, int, byte[])[]? IconPixmap { get; set; } + public (int, int, byte[])[] IconPixmap { get; set; } = { EmptyPixmap }; public string? OverlayIconName { get; set; } public (int, int, byte[])[]? OverlayIconPixmap { get; set; } public string? AttentionIconName { get; set; } public (int, int, byte[])[]? AttentionIconPixmap { get; set; } public string? AttentionMovieName { get; set; } public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } + + public static (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); } } } diff --git a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs index a0ca8abc97..23f1dcf0e7 100644 --- a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs +++ b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs @@ -56,8 +56,8 @@ namespace Avalonia.FreeDesktop public ValueTask WatchActionInvokedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", - (m, s) => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", static (m, s) + => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); public Task SetVersionAsync(uint value) { @@ -81,15 +81,15 @@ namespace Avalonia.FreeDesktop } public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static NotificationProperties ReadMessage(Message message, DesktopObject _) + static NotificationProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -99,10 +99,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, - emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -114,10 +114,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -131,10 +131,10 @@ namespace Avalonia.FreeDesktop } } - private static NotificationProperties ReadProperties(ref Reader reader, List? changedList = null) + private static NotificationProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new NotificationProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -168,7 +168,7 @@ namespace Avalonia.FreeDesktop public Task OpenUriAsync(string parentWindow, string uri, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -188,7 +188,7 @@ namespace Avalonia.FreeDesktop public Task OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -208,7 +208,7 @@ namespace Avalonia.FreeDesktop public Task OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -248,15 +248,15 @@ namespace Avalonia.FreeDesktop } public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static OpenURIProperties ReadMessage(Message message, DesktopObject _) + static OpenURIProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -266,10 +266,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, - emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -280,10 +280,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -297,10 +297,10 @@ namespace Avalonia.FreeDesktop } } - private static OpenURIProperties ReadProperties(ref Reader reader, List? changedList = null) + private static OpenURIProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new OpenURIProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -356,7 +356,7 @@ namespace Avalonia.FreeDesktop public Task PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -377,7 +377,8 @@ namespace Avalonia.FreeDesktop public Task RequestInstallTokenAsync(string name, object iconV, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) + => ReadMessage_s(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -416,7 +417,8 @@ namespace Avalonia.FreeDesktop public Task GetDesktopEntryAsync(string desktopFileId) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) + => ReadMessage_s(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -434,7 +436,8 @@ namespace Avalonia.FreeDesktop public Task<(object IconV, string IconFormat, uint IconSize)> GetIconAsync(string desktopFileId) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_vsu(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) + => ReadMessage_vsu(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -512,19 +515,19 @@ namespace Avalonia.FreeDesktop } public Task GetSupportedLauncherTypesAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static DynamicLauncherProperties ReadMessage(Message message, DesktopObject _) + static DynamicLauncherProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -534,10 +537,11 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -549,10 +553,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -569,10 +573,10 @@ namespace Avalonia.FreeDesktop } } - private static DynamicLauncherProperties ReadProperties(ref Reader reader, List? changedList = null) + private static DynamicLauncherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new DynamicLauncherProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -611,7 +615,7 @@ namespace Avalonia.FreeDesktop public Task OpenFileAsync(string parentWindow, string title, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -631,7 +635,7 @@ namespace Avalonia.FreeDesktop public Task SaveFileAsync(string parentWindow, string title, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -651,7 +655,7 @@ namespace Avalonia.FreeDesktop public Task SaveFilesAsync(string parentWindow, string title, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -691,15 +695,15 @@ namespace Avalonia.FreeDesktop } public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static FileChooserProperties ReadMessage(Message message, DesktopObject _) + static FileChooserProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -709,10 +713,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, - emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -724,10 +728,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -741,10 +745,10 @@ namespace Avalonia.FreeDesktop } } - private static FileChooserProperties ReadProperties(ref Reader reader, List? changedList = null) + private static FileChooserProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new FileChooserProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -811,8 +815,8 @@ namespace Avalonia.FreeDesktop protected DesktopObject(DesktopService service, ObjectPath path) => (Service, Path) = (service, path); - public DesktopService Service { get; } - public ObjectPath Path { get; } + protected DesktopService Service { get; } + protected ObjectPath Path { get; } protected Connection Connection => Service.Connection; protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) @@ -855,13 +859,11 @@ namespace Avalonia.FreeDesktop Member = "PropertiesChanged", Arg0 = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, changes, rs, hs) => - ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) + => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule @@ -872,9 +874,8 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } protected static ObjectPath ReadMessage_o(Message message, DesktopObject _) diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs index 9fa9c1809e..6ae63ea322 100644 --- a/src/Avalonia.FreeDesktop/IX11InputMethod.cs +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -19,7 +19,7 @@ namespace Avalonia.FreeDesktop public KeyModifiers Modifiers { get; set; } public RawKeyEventType Type { get; set; } } - + public interface IX11InputMethodControl : IDisposable { void SetWindowActive(bool active); @@ -27,7 +27,7 @@ namespace Avalonia.FreeDesktop ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode); event Action Commit; event Action ForwardKey; - + void UpdateWindowInfo(PixelPoint position, double scaling); } } diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs index 147955b6a3..df22f46323 100644 --- a/src/Avalonia.FreeDesktop/NativeMethods.cs +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -15,18 +15,18 @@ namespace Avalonia.FreeDesktop public static string ReadLink(string path) { var symlinkSize = Encoding.UTF8.GetByteCount(path); - var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated + const int BufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated var symlink = ArrayPool.Shared.Rent(symlinkSize + 1); - var buffer = ArrayPool.Shared.Rent(bufferSize); + var buffer = ArrayPool.Shared.Rent(BufferSize); try { Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); symlink[symlinkSize] = 0; - var size = readlink(symlink, buffer, bufferSize); - Debug.Assert(size < bufferSize); // if this fails, we need to increase the buffer size (dynamically?) + var size = readlink(symlink, buffer, BufferSize); + Debug.Assert(size < BufferSize); // if this fails, we need to increase the buffer size (dynamically?) return Encoding.UTF8.GetString(buffer, 0, (int)size); } diff --git a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs index 5cd44905af..4e6ce3bd43 100644 --- a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs +++ b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs @@ -145,10 +145,10 @@ namespace Avalonia.FreeDesktop public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) - => ReadMessage(m, (StatusNotifierWatcherObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static StatusNotifierWatcherProperties ReadMessage(Message message, StatusNotifierWatcherObject _) + static StatusNotifierWatcherProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -157,9 +157,9 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, static (m, s) => ReadMessage(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, StatusNotifierWatcherObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -194,7 +194,7 @@ namespace Avalonia.FreeDesktop } } - private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, List? changedList = null) + private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new StatusNotifierWatcherProperties(); var headersEnd = reader.ReadArrayStart(DBusType.Struct); @@ -245,8 +245,8 @@ namespace Avalonia.FreeDesktop (Service, Path) = (service, path); } - public StatusNotifierWatcherService Service { get; } - public ObjectPath Path { get; } + protected StatusNotifierWatcherService Service { get; } + protected ObjectPath Path { get; } protected Connection Connection => Service.Connection; protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) @@ -291,7 +291,7 @@ namespace Avalonia.FreeDesktop ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { @@ -301,12 +301,11 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) + => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { From e5985aa675221931c25cbc27b168b9fac3a254a4 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 30 Dec 2022 14:34:31 +0100 Subject: [PATCH 03/36] Import TrimmingEnable.props --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a33fa7b32d..e80a90e38a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,6 +5,9 @@ enable + + + @@ -15,7 +18,9 @@ + + From 2c2eed724eb6e821b41d0d61134737bdb4525068 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:26:36 +0100 Subject: [PATCH 04/36] Use source generator --- .gitmodules | 3 + Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 + .../AppMenuRegistrar.DBus.cs | 173 ---- .../Avalonia.FreeDesktop.csproj | 5 + src/Avalonia.FreeDesktop/DBus.DBus.cs | 641 ------------ .../DBusIme/DBusTextInputMethodBase.cs | 2 +- .../DBusIme/Fcitx/Fcitx.DBus.cs | 627 ------------ .../DBusIme/Fcitx/FcitxICWrapper.cs | 10 +- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 9 +- .../DBusIme/IBus/IBus.DBus.cs | 513 ---------- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 12 +- src/Avalonia.FreeDesktop/DBusInterfaces.cs | 18 + src/Avalonia.FreeDesktop/DBusMenu.DBus.cs | 500 ---------- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 178 +--- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 18 +- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 190 +--- .../FreeDesktopPortalDesktop.DBus.cs | 938 ------------------ .../StatusNotifierWatcher.DBus.cs | 350 ------- src/tools/Tmds.DBus.SourceGenerator | 1 + 20 files changed, 130 insertions(+), 4066 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBus.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBusInterfaces.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusMenu.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs create mode 160000 src/tools/Tmds.DBus.SourceGenerator diff --git a/.gitmodules b/.gitmodules index 16bc977251..2b899e8f5a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "Tmds.DBus"] path = src/Linux/Tmds.DBus url = https://github.com/affederaffe/Tmds.DBus +[submodule "src/tools/Tmds.DBus.SourceGenerator"] + path = src/tools/Tmds.DBus.SourceGenerator + url = https://github.com/affederaffe/Tmds.DBus.SourceGenerator diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 72eb13d0a9..165245781b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -38,6 +38,7 @@ "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator.csproj", "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index e399483896..47ed2b9140 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -233,6 +233,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.Protocol", "src\Linux\Tmds.DBus\src\Tmds.DBus.Protocol\Tmds.DBus.Protocol.csproj", "{29E25263-3CC3-4D55-A042-00BA136867D4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.SourceGenerator", "src\tools\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj", "{5E9C0032-E460-4BC1-BCC7-6448F34DD679}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -548,6 +550,10 @@ Global {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.Build.0 = Release|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -613,6 +619,7 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29E25263-3CC3-4D55-A042-00BA136867D4} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {5E9C0032-E460-4BC1-BCC7-6448F34DD679} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs deleted file mode 100644 index 75c8ce61a5..0000000000 --- a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal class Registrar : AppMenuRegistrarObject - { - private const string Interface = "com.canonical.AppMenu.Registrar"; - - public Registrar(RegistrarService service, ObjectPath path) : base(service, path) { } - - public Task RegisterWindowAsync(uint windowId, ObjectPath menuObjectPath) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uo", - member: "RegisterWindow", - flags: MessageFlags.NoReplyExpected); - writer.WriteUInt32(windowId); - writer.WriteObjectPath(menuObjectPath); - return writer.CreateMessage(); - } - } - - public Task UnregisterWindowAsync(uint windowId) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "UnregisterWindow"); - writer.WriteUInt32(windowId); - return writer.CreateMessage(); - } - } - - public Task<(string Service, ObjectPath MenuObjectPath)> GetMenuForWindowAsync(uint windowId) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "GetMenuForWindow"); - writer.WriteUInt32(windowId); - return writer.CreateMessage(); - } - } - } - - internal class RegistrarService - { - public RegistrarService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public Registrar CreateRegistrar(string path) => new(this, path); - } - - internal class AppMenuRegistrarObject - { - protected AppMenuRegistrarObject(RegistrarService service, ObjectPath path) - => (Service, Path) = (service, path); - - protected RegistrarService Service { get; } - protected ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) - => ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) - => null!, static (ex, _, _, hs) - => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static (string, ObjectPath) ReadMessage_so(Message message, AppMenuRegistrarObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadObjectPath(); - return (arg0, arg1); - } - } -} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index e80a90e38a..e1e0a636ea 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -17,10 +17,15 @@ + + + + + diff --git a/src/Avalonia.FreeDesktop/DBus.DBus.cs b/src/Avalonia.FreeDesktop/DBus.DBus.cs deleted file mode 100644 index 0f83ddaa4a..0000000000 --- a/src/Avalonia.FreeDesktop/DBus.DBus.cs +++ /dev/null @@ -1,641 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal record DBusProperties - { - public string[] Features { get; set; } = default!; - public string[] Interfaces { get; set; } = default!; - } - - internal class DBus : DBusObject - { - private const string Interface = "org.freedesktop.DBus"; - - public DBus(DBusService service, ObjectPath path) : base(service, path) { } - - public Task HelloAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Hello"); - return writer.CreateMessage(); - } - } - - public Task RequestNameAsync(string a0, uint a1) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "su", - member: "RequestName"); - writer.WriteString(a0); - writer.WriteUInt32(a1); - return writer.CreateMessage(); - } - } - - public Task ReleaseNameAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "ReleaseName"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task StartServiceByNameAsync(string a0, uint a1) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "su", - member: "StartServiceByName"); - writer.WriteString(a0); - writer.WriteUInt32(a1); - return writer.CreateMessage(); - } - } - - public Task UpdateActivationEnvironmentAsync(Dictionary a0) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "a{ss}", - member: "UpdateActivationEnvironment"); - writer.WriteDictionary(a0); - return writer.CreateMessage(); - } - } - - public Task NameHasOwnerAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "NameHasOwner"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task ListNamesAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "ListNames"); - return writer.CreateMessage(); - } - } - - public Task ListActivatableNamesAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "ListActivatableNames"); - return writer.CreateMessage(); - } - } - - public Task AddMatchAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "AddMatch"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task RemoveMatchAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RemoveMatch"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetNameOwnerAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetNameOwner"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task ListQueuedOwnersAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "ListQueuedOwners"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetConnectionUnixUserAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionUnixUser"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetConnectionUnixProcessIDAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionUnixProcessID"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetAdtAuditSessionDataAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetAdtAuditSessionData"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetConnectionSELinuxSecurityContextAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionSELinuxSecurityContext"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task ReloadConfigAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "ReloadConfig"); - return writer.CreateMessage(); - } - } - - public Task GetIdAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "GetId"); - return writer.CreateMessage(); - } - } - - public Task> GetConnectionCredentialsAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_aesv(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionCredentials"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public ValueTask WatchNameOwnerChangedAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "NameOwnerChanged", static (m, s) => - ReadMessage_sss(m, (DBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchNameLostAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "NameLost", static (m, s) => - ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchNameAcquiredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "NameAcquired", static (m, s) => - ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); - - public Task SetFeaturesAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Features"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task SetInterfacesAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Interfaces"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task GetFeaturesAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Features"), static (m, s) => - ReadMessage_v_as(m, (DBusObject)s!), this); - - public Task GetInterfacesAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Interfaces"), static (m, s) => - ReadMessage_v_as(m, (DBusObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => - ReadMessage(m), this); - - static DBusProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) => - ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "Features": - invalidated.Add("Features"); - break; - case "Interfaces": - invalidated.Add("Interfaces"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static DBusProperties ReadProperties(ref Reader reader, List? changedList = null) - { - var props = new DBusProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "Features": - reader.ReadSignature("as"); - props.Features = reader.ReadArray(); - changedList?.Add("Features"); - break; - case "Interfaces": - reader.ReadSignature("as"); - props.Interfaces = reader.ReadArray(); - changedList?.Add("Interfaces"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal class DBusService - { - public DBusService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public DBus CreateDBus(string path) => new(this, path); - } - - internal class DBusObject - { - protected DBusObject(DBusService service, ObjectPath path) - { - Service = service; - Path = path; - } - - public DBusService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => - ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static string ReadMessage_s(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static uint ReadMessage_u(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadUInt32(); - } - - protected static bool ReadMessage_b(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static string[] ReadMessage_as(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadArray(); - } - - protected static byte[] ReadMessage_ay(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadArray(); - } - - protected static Dictionary ReadMessage_aesv(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadDictionary(); - } - - protected static (string, string, string) ReadMessage_sss(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadString(); - return (arg0, arg1, arg2); - } - - protected static string[] ReadMessage_v_as(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("as"); - return reader.ReadArray(); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index c7bf530cfd..08d2a0c219 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -60,7 +60,7 @@ namespace Avalonia.FreeDesktop.DBusIme { foreach (var name in _knownNames) { - var dbus = new DBusService(Connection, name).CreateDBus("/org/freedesktop/DBus"); + var dbus = new OrgFreedesktopDBus(Connection, name, "/org/freedesktop/DBus"); _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); var nameOwner = await dbus.GetNameOwnerAsync(name); OnNameChange(null, (name, null, nameOwner)); diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs deleted file mode 100644 index f9e4497892..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs +++ /dev/null @@ -1,627 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop.DBusIme.Fcitx -{ - internal class InputContext : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputContext"; - - public InputContext(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task FocusInAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusIn"); - return writer.CreateMessage(); - } - } - - public Task FocusOutAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusOut"); - return writer.CreateMessage(); - } - } - - public Task ResetAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Reset"); - return writer.CreateMessage(); - } - } - - public Task SetCursorRectAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorRect"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task SetCapacityAsync(uint caps) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "SetCapacity"); - writer.WriteUInt32(caps); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "suu", - member: "SetSurroundingText"); - writer.WriteString(text); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uu", - member: "SetSurroundingTextPosition"); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task DestroyICAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "DestroyIC"); - return writer.CreateMessage(); - } - } - - public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, int type, uint time) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_i(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uuuiu", - member: "ProcessKeyEvent"); - writer.WriteUInt32(keyval); - writer.WriteUInt32(keycode); - writer.WriteUInt32(state); - writer.WriteInt32(type); - writer.WriteUInt32(time); - return writer.CreateMessage(); - } - } - - public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => - ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchCurrentIMAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => - ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => - ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchForwardKeyAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => - ReadMessage_uui(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchDeleteSurroundingTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => - ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); - } - - internal class InputMethod : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputMethod"; - - public InputMethod(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task<(int Icid, bool Enable, uint Keyval1, uint State1, uint Keyval2, uint State2)> CreateICv3Async(string appname, int pid) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ibuuuu(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "si", - member: "CreateICv3"); - writer.WriteString(appname); - writer.WriteInt32(pid); - return writer.CreateMessage(); - } - } - } - - internal class InputContext1 : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputContext1"; - - public InputContext1(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task FocusInAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusIn"); - return writer.CreateMessage(); - } - } - - public Task FocusOutAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusOut"); - return writer.CreateMessage(); - } - } - - public Task ResetAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Reset"); - return writer.CreateMessage(); - } - } - - public Task SetCursorRectAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorRect"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task SetCapabilityAsync(ulong caps) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "t", - member: "SetCapability"); - writer.WriteUInt64(caps); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "suu", - member: "SetSurroundingText"); - writer.WriteString(text); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uu", - member: "SetSurroundingTextPosition"); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task DestroyICAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "DestroyIC"); - return writer.CreateMessage(); - } - } - - public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, bool type, uint time) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uuubu", - member: "ProcessKeyEvent"); - writer.WriteUInt32(keyval); - writer.WriteUInt32(keycode); - writer.WriteUInt32(state); - writer.WriteBool(type); - writer.WriteUInt32(time); - return writer.CreateMessage(); - } - } - - public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => - ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchCurrentIMAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => - ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => - ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchForwardKeyAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => - ReadMessage_uub(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchDeleteSurroundingTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => - ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); - } - - internal class InputMethod1 : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputMethod1"; - - public InputMethod1(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task<(ObjectPath A0, byte[] A1)> CreateInputContextAsync((string, string)[] a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_oay(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "a(ss)", - member: "CreateInputContext"); - writer.WriteArray(a0); - return writer.CreateMessage(); - } - } - } - - internal class FcitxService - { - public FcitxService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public InputContext CreateInputContext(string path) => new(this, path); - public InputMethod CreateInputMethod(string path) => new(this, path); - public InputContext1 CreateInputContext1(string path) => new(this, path); - public InputMethod1 CreateInputMethod1(string path) => new(this, path); - } - - internal class FcitxObject - { - protected FcitxObject(FcitxService service, ObjectPath path) - => (Service, Path) = (service, path); - - public FcitxService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) => - null!, static (ex, _, _, hs) => - ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static int ReadMessage_i(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadInt32(); - } - - protected static string ReadMessage_s(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static (string, string, string) ReadMessage_sss(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadString(); - return (arg0, arg1, arg2); - } - - protected static ((string, int)[], int) ReadMessage_arsizi(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadArray<(string, int)>(); - var arg1 = reader.ReadInt32(); - return (arg0, arg1); - } - - protected static (uint, uint, int) ReadMessage_uui(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadInt32(); - return (arg0, arg1, arg2); - } - - protected static (int, uint) ReadMessage_iu(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadInt32(); - var arg1 = reader.ReadUInt32(); - return (arg0, arg1); - } - - protected static (int, bool, uint, uint, uint, uint) ReadMessage_ibuuuu(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadInt32(); - var arg1 = reader.ReadBool(); - var arg2 = reader.ReadUInt32(); - var arg3 = reader.ReadUInt32(); - var arg4 = reader.ReadUInt32(); - var arg5 = reader.ReadUInt32(); - return (arg0, arg1, arg2, arg3, arg4, arg5); - } - - protected static bool ReadMessage_b(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static (uint, uint, bool) ReadMessage_uub(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadBool(); - return (arg0, arg1, arg2); - } - - protected static (ObjectPath, byte[]) ReadMessage_oay(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadObjectPath(); - var arg1 = reader.ReadArray(); - return (arg0, arg1); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index b71ca55cf8..b651f126fb 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,15 +5,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly InputContext1? _modern; - private readonly InputContext? _old; + private readonly OrgFcitxFcitxInputContext1? _modern; + private readonly OrgFcitxFcitxInputContext? _old; - public FcitxICWrapper(InputContext old) + public FcitxICWrapper(OrgFcitxFcitxInputContext old) { _old = old; } - public FcitxICWrapper(InputContext1 modern) + public FcitxICWrapper(OrgFcitxFcitxInputContext1 modern) { _modern = modern; } @@ -43,7 +43,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx public ValueTask WatchForwardKeyAsync(Action 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(default(IDisposable?)); public Task SetCapacityAsync(uint flags) => diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 2a5260b1cf..6b30c5424a 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -17,21 +17,20 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx protected override async Task Connect(string name) { - var service = new FcitxService(Connection, name); if (name == "org.fcitx.Fcitx") { - var method = service.CreateInputMethod("/inputmethod"); + var method = new OrgFcitxFcitxInputMethod(Connection, name, "/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = service.CreateInputContext($"/inputcontext_{resp.Icid}"); + var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = service.CreateInputMethod1("/inputmethod"); + var method = new OrgFcitxFcitxInputMethod1(Connection, name, "/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = service.CreateInputContext1(resp.A0); + var proxy = new OrgFcitxFcitxInputContext1(Connection, name, resp.Item1); _context = new FcitxICWrapper(proxy); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs deleted file mode 100644 index e0f9681766..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs +++ /dev/null @@ -1,513 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop.DBusIme.IBus -{ - internal class Portal : IBusObject - { - private const string Interface = "org.freedesktop.IBus.Portal"; - - public Portal(IBusService service, ObjectPath path) : base(service, path) { } - - public Task CreateInputContextAsync(string clientName) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (IBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "CreateInputContext"); - writer.WriteString(clientName); - return writer.CreateMessage(); - } - } - } - - internal class InputContext : IBusObject - { - private const string Interface = "org.freedesktop.IBus.InputContext"; - - public InputContext(IBusService service, ObjectPath path) : base(service, path) { } - - public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (IBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uuu", - member: "ProcessKeyEvent"); - writer.WriteUInt32(keyval); - writer.WriteUInt32(keycode); - writer.WriteUInt32(state); - return writer.CreateMessage(); - } - } - - public Task SetCursorLocationAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorLocation"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task SetCursorLocationRelativeAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorLocationRelative"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task ProcessHandWritingEventAsync(double[] coordinates) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ad", - member: "ProcessHandWritingEvent"); - writer.WriteArray(coordinates); - return writer.CreateMessage(); - } - } - - public Task CancelHandWritingAsync(uint nStrokes) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "CancelHandWriting"); - writer.WriteUInt32(nStrokes); - return writer.CreateMessage(); - } - } - - public Task FocusInAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusIn"); - return writer.CreateMessage(); - } - } - - public Task FocusOutAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusOut"); - return writer.CreateMessage(); - } - } - - public Task ResetAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Reset"); - return writer.CreateMessage(); - } - } - - public Task SetCapabilitiesAsync(uint caps) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "SetCapabilities"); - writer.WriteUInt32(caps); - return writer.CreateMessage(); - } - } - - public Task PropertyActivateAsync(string name, uint state) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "su", - member: "PropertyActivate"); - writer.WriteString(name); - writer.WriteUInt32(state); - return writer.CreateMessage(); - } - } - - public Task SetEngineAsync(string name) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "SetEngine"); - writer.WriteString(name); - return writer.CreateMessage(); - } - } - - public Task GetEngineAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_v(m, (IBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "GetEngine"); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextAsync(object text, uint cursorPos, uint anchorPos) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "vuu", - member: "SetSurroundingText"); - writer.WriteVariant(text); - writer.WriteUInt32(cursorPos); - writer.WriteUInt32(anchorPos); - return writer.CreateMessage(); - } - } - - public ValueTask WatchCommitTextAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CommitText", static (m, s) => - ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchForwardKeyEventAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKeyEvent", static (m, s) => - ReadMessage_uuu(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdatePreeditTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditText", static (m, s) => - ReadMessage_vub(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdatePreeditTextWithModeAsync( - Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditTextWithMode", static (m, s) => - ReadMessage_vubu(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchShowPreeditTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ShowPreeditText", handler, emitOnCapturedContext); - - public ValueTask WatchHidePreeditTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "HidePreeditText", handler, emitOnCapturedContext); - - public ValueTask WatchUpdateAuxiliaryTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateAuxiliaryText", static (m, s) => - ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchShowAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ShowAuxiliaryText", handler, emitOnCapturedContext); - - public ValueTask WatchHideAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "HideAuxiliaryText", handler, emitOnCapturedContext); - - public ValueTask WatchUpdateLookupTableAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateLookupTable", static (m, s) => - ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchShowLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ShowLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchHideLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "HideLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchPageUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "PageUpLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchPageDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "PageDownLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchCursorUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "CursorUpLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchCursorDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "CursorDownLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchRegisterPropertiesAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "RegisterProperties", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdatePropertyAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "UpdateProperty", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); - } - - internal class Service : IBusObject - { - private const string Interface = "org.freedesktop.IBus.Service"; - - public Service(IBusService service, ObjectPath path) : base(service, path) { } - - public Task DestroyAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Destroy"); - return writer.CreateMessage(); - } - } - } - - internal class IBusService - { - public IBusService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public Portal CreatePortal(string path) => new(this, path); - public InputContext CreateInputContext(string path) => new(this, path); - public Service CreateService(string path) => new(this, path); - } - - internal class IBusObject - { - protected IBusObject(IBusService service, ObjectPath path) - => (Service, Path) = (service, path); - - public IBusService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static ObjectPath ReadMessage_o(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadObjectPath(); - } - - protected static bool ReadMessage_b(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static object ReadMessage_v(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadVariant(); - } - - protected static (uint, uint, uint) ReadMessage_uuu(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadUInt32(); - return (arg0, arg1, arg2); - } - - protected static (object, uint, bool) ReadMessage_vub(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadBool(); - return (arg0, arg1, arg2); - } - - protected static (object, uint, bool, uint) ReadMessage_vubu(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadBool(); - var arg3 = reader.ReadUInt32(); - return (arg0, arg1, arg2, arg3); - } - - protected static (object, bool) ReadMessage_vb(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadBool(); - return (arg0, arg1); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index f3ab8617a4..4db0d95d3c 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -9,17 +9,17 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private Service? _service; - private InputContext? _context; + private OrgFreedesktopIBusService? _service; + private OrgFreedesktopIBusInputContext? _context; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var service = new IBusService(Connection, name); - var path = await service.CreatePortal("/org/freedesktop/IBus").CreateInputContextAsync(GetAppName()); - _context = service.CreateInputContext(path); - _service = service.CreateService(path); + var portal = new OrgFreedesktopIBusPortal(Connection, name, "/org/freedesktop/IBus"); + var path = await portal.CreateInputContextAsync(GetAppName()); + _service = new OrgFreedesktopIBusService(Connection, name, path); + _context = new OrgFreedesktopIBusInputContext(Connection, name, path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); diff --git a/src/Avalonia.FreeDesktop/DBusInterfaces.cs b/src/Avalonia.FreeDesktop/DBusInterfaces.cs new file mode 100644 index 0000000000..3bece27ab1 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusInterfaces.cs @@ -0,0 +1,18 @@ +using Tmds.DBus.SourceGenerator; + +namespace Avalonia.FreeDesktop +{ + [DBusInterface("./DBusXml/DBus.xml")] + [DBusInterface("./DBusXml/StatusNotifierWatcher.xml")] + [DBusInterface("./DBusXml/com.canonical.AppMenu.Registrar.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext1.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod1.xml")] + [DBusInterface("./DBusXml/org.freedesktop.portal.FileChooser.xml")] + [DBusInterface("./DBusXml/org.freedesktop.portal.Request.xml")] + [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] + [DBusHandler("./DBusXml/DBusMenu.xml")] + [DBusHandler("./DBusXml/StatusNotifierItem.xml")] + internal class DBusInterfaces { } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs deleted file mode 100644 index e3928cfbb2..0000000000 --- a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs +++ /dev/null @@ -1,500 +0,0 @@ -using System; -using Tmds.DBus.Protocol; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Avalonia.FreeDesktop -{ - internal record DBusMenuProperties - { - public uint Version { get; set; } - public string TextDirection { get; set; } = default!; - public string Status { get; set; } = default!; - public string[] IconThemePath { get; set; } = default!; - } - - internal class DBusMenu : DBusMenuObject - { - private const string Interface = "com.canonical.dbusmenu"; - - public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) { } - - public Task<(uint Revision, (int, Dictionary, object[]) Layout)> GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_uriaesvavz(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iias", - member: "GetLayout"); - writer.WriteInt32(parentId); - writer.WriteInt32(recursionDepth); - writer.WriteArray(propertyNames); - return writer.CreateMessage(); - } - } - - public Task<(int, Dictionary)[]> GetGroupPropertiesAsync(int[] ids, string[] propertyNames) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ariaesvz(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "aias", - member: "GetGroupProperties"); - writer.WriteArray(ids); - writer.WriteArray(propertyNames); - return writer.CreateMessage(); - } - } - - public Task GetPropertyAsync(int id, string name) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_v(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "is", - member: "GetProperty"); - writer.WriteInt32(id); - writer.WriteString(name); - return writer.CreateMessage(); - } - } - - public Task EventAsync(int id, string eventId, object data, uint timestamp) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "isvu", - member: "Event"); - writer.WriteInt32(id); - writer.WriteString(eventId); - writer.WriteVariant(data); - writer.WriteUInt32(timestamp); - return writer.CreateMessage(); - } - } - - public Task EventGroupAsync((int, string, object, uint)[] events) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ai(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "a(isvu)", - member: "EventGroup"); - writer.WriteArray(events); - return writer.CreateMessage(); - } - } - - public Task AboutToShowAsync(int id) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_b(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "i", - member: "AboutToShow"); - writer.WriteInt32(id); - return writer.CreateMessage(); - } - } - - public Task<(int[] UpdatesNeeded, int[] IdErrors)> AboutToShowGroupAsync(int[] ids) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_aiai(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ai", - member: "AboutToShowGroup"); - writer.WriteArray(ids); - return writer.CreateMessage(); - } - } - - public ValueTask WatchItemsPropertiesUpdatedAsync(Action)[] UpdatedProps, (int, string[])[] RemovedProps)> handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", static (m, _) => ReadMessage_ariaesvzariasz(m), handler, emitOnCapturedContext); - - public ValueTask WatchLayoutUpdatedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", static (m, _) => ReadMessage_ui(m), handler, emitOnCapturedContext); - - public ValueTask WatchItemActivationRequestedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", static (m, _) => ReadMessage_iu(m), handler, emitOnCapturedContext); - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task SetTextDirectionAsync(string value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("TextDirection"); - writer.WriteSignature("s"); - writer.WriteString(value); - return writer.CreateMessage(); - } - } - - public Task SetStatusAsync(string value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Status"); - writer.WriteSignature("s"); - writer.WriteString(value); - return writer.CreateMessage(); - } - } - - public Task SetIconThemePathAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("IconThemePath"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), static (m, _) => ReadMessage_v_u(m), this); - - public Task GetTextDirectionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), static (m, _) => ReadMessage_v_s(m), this); - - public Task GetStatusAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), static (m, _) => ReadMessage_v_s(m), this); - - public Task GetIconThemePathAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), static (m, _) => ReadMessage_v_as(m), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => ReadMessage(m), this); - static DBusMenuProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); - } - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "Version": invalidated.Add("Version"); break; - case "TextDirection": invalidated.Add("TextDirection"); break; - case "Status": invalidated.Add("Status"); break; - case "IconThemePath": invalidated.Add("IconThemePath"); break; - } - } - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static DBusMenuProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new DBusMenuProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "Version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - case "TextDirection": - reader.ReadSignature("s"); - props.TextDirection = reader.ReadString(); - changedList?.Add("TextDirection"); - break; - case "Status": - reader.ReadSignature("s"); - props.Status = reader.ReadString(); - changedList?.Add("Status"); - break; - case "IconThemePath": - reader.ReadSignature("as"); - props.IconThemePath = reader.ReadArray(); - changedList?.Add("IconThemePath"); - break; - default: - reader.ReadVariant(); - break; - } - } - return props; - } - } - - internal class DBusMenuService - { - public Connection Connection { get; } - public string Destination { get; } - public DBusMenuService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - public DBusMenu CreateDbusmenu(string path) => new(this, path); - } - - internal class DBusMenuObject - { - public DBusMenuService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - protected DBusMenuObject(DBusMenuService service, ObjectPath path) - => (Service, Path) = (service, path); - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) - => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) - => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) - => null!, static (ex, _, _, hs) - => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadStruct, object[]>(); - return (arg0, arg1); - } - - protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadArray<(int, Dictionary)>(); - } - - protected static object ReadMessage_v(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadVariant(); - } - - protected static int[] ReadMessage_ai(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadArray(); - } - - protected static bool ReadMessage_b(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static (int[], int[]) ReadMessage_aiai(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadArray(); - var arg1 = reader.ReadArray(); - return (arg0, arg1); - } - - protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadArray<(int, Dictionary)>(); - var arg1 = reader.ReadArray<(int, string[])>(); - return (arg0, arg1); - } - - protected static (uint, int) ReadMessage_ui(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadInt32(); - return (arg0, arg1); - } - - protected static (int, uint) ReadMessage_iu(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadInt32(); - var arg1 = reader.ReadUInt32(); - return (arg0, arg1); - } - - protected static uint ReadMessage_v_u(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("u"); - return reader.ReadUInt32(); - } - - protected static string ReadMessage_v_s(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("s"); - return reader.ReadString(); - } - - protected static string[] ReadMessage_v_as(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("as"); - return reader.ReadArray(); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 69e2eed72f..e8d4a7b6b4 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -25,16 +24,15 @@ namespace Avalonia.FreeDesktop public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IMethodHandler, IDisposable + private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { private readonly Connection _connection; - private readonly DBusMenuProperties _dBusMenuProperties = new() { Version = 2, Status = "normal" }; private readonly Dictionary _idsToItems = new(); private readonly Dictionary _itemsToIds = new(); private readonly HashSet _menus = new(); private readonly uint _xid; private readonly bool _appMenu = true; - private Registrar? _registrar; + private ComCanonicalAppMenuRegistrar? _registrar; private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; @@ -59,7 +57,40 @@ namespace Avalonia.FreeDesktop Init(); } - public string Path { get; } + public override string Path { get; } + + protected override (uint revision, (int, Dictionary, object[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) + { + var menu = GetMenu(parentId); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); + } + + return (_revision, layout); + } + + protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => + ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray(); + + protected override object OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? 0; + + protected override void OnEvent(int id, string eventId, object data, uint timestamp) => + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); + + protected override int[] OnEventGroup((int, string, object, uint)[] events) + { + foreach (var e in events) + Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2, e.Item3, e.Item4)); + return Array.Empty(); + } + + protected override bool OnAboutToShow(int id) => false; + + protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) => + (Array.Empty(), Array.Empty()); private async void Init() { @@ -69,8 +100,7 @@ namespace Avalonia.FreeDesktop var services = await _connection.ListServicesAsync(); if (!services.Contains("com.canonical.AppMenu.Registrar")) return; - _registrar = new RegistrarService(_connection, "com.canonical.AppMenu.Registrar") - .CreateRegistrar("/com/canonical/AppMenu/Registrar"); + _registrar = new ComCanonicalAppMenuRegistrar(_connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); if (!_disposed) await _registrar.RegisterWindowAsync(_xid, Path); // It's not really important if this code succeeds, @@ -297,140 +327,6 @@ namespace Avalonia.FreeDesktop } } - public ValueTask HandleMethodAsync(MethodContext context) - { - switch (context.Request.InterfaceAsString) - { - case "com.canonical.dbusmenu": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("GetLayout", "iias"): - { - using var writer = context.CreateReplyWriter("u(ia{sv}av)"); - var reader = context.Request.GetBodyReader(); - var parentId = reader.ReadInt32(); - var recursionDepth = reader.ReadInt32(); - var propertyNames = reader.ReadArray(); - var menu = GetMenu(parentId); - var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); - writer.WriteUInt32(_revision); - writer.WriteStruct(layout); - if (!IsNativeMenuExported) - { - IsNativeMenuExported = true; - Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); - } - - context.Reply(writer.CreateMessage()); - break; - } - case ("GetGroupProperties", "aias"): - { - using var writer = context.CreateReplyWriter("a(ia{sv})"); - var reader = context.Request.GetBodyReader(); - var ids = reader.ReadArray(); - var propertyNames = reader.ReadArray(); - var arrayStart = writer.WriteArrayStart(DBusType.Struct); - foreach (var id in ids) - { - var item = GetMenu(id); - var props = GetProperties(item, propertyNames); - writer.WriteStruct((id, props)); - } - - writer.WriteArrayEnd(arrayStart); - context.Reply(writer.CreateMessage()); - break; - } - case ("GetProperty", "is"): - { - using var writer = context.CreateReplyWriter("v"); - var reader = context.Request.GetBodyReader(); - var id = reader.ReadInt32(); - var name = reader.ReadString(); - writer.WriteVariant(GetProperty(GetMenu(id), name) ?? 0); - context.Reply(writer.CreateMessage()); - break; - } - case ("Event", "isvu"): - { - var reader = context.Request.GetBodyReader(); - var id = reader.ReadInt32(); - var eventId = reader.ReadString(); - var data = reader.ReadVariant(); - var timestamp = reader.ReadUInt32(); - Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); - break; - } - case ("EventGroup", "a(isvu)"): - { - using var writer = context.CreateReplyWriter("ai"); - var reader = context.Request.GetBodyReader(); - var events = reader.ReadArray<(int Id, string EventId, object Data, uint Timestamp)>(); - foreach (var e in events) - Dispatcher.UIThread.Post(() => HandleEvent(e.Id, e.EventId, e.Data, e.Timestamp)); - writer.WriteArray(Array.Empty()); - context.Reply(writer.CreateMessage()); - break; - } - case ("AboutToShow", "i"): - { - using var writer = context.CreateReplyWriter("b"); - writer.WriteBool(false); - context.Reply(writer.CreateMessage()); - break; - } - case ("AboutToShowGroup", "ai"): - { - using var writer = context.CreateReplyWriter("aiai"); - writer.WriteStruct((Array.Empty(), Array.Empty())); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - case "org.freedesktop.DBus.Properties": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("Version", "u"): - { - using var writer = context.CreateReplyWriter("u"); - writer.WriteUInt32(_dBusMenuProperties.Version); - context.Reply(writer.CreateMessage()); - break; - } - case ("TextDirection", "s"): - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_dBusMenuProperties.TextDirection); - context.Reply(writer.CreateMessage()); - break; - } - case ("Status", "s"): - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_dBusMenuProperties.Status); - context.Reply(writer.CreateMessage()); - break; - } - case ("IconThemePath", "as"): - { - using var writer = context.CreateReplyWriter("as"); - writer.WriteArray(_dBusMenuProperties.IconThemePath); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - } - - return default; - } - - public bool RunMethodHandlerSynchronously(Message message) => true; - private void EmitUIntIntSignal(string member, uint arg0, int arg1) { using var writer = _connection.GetMessageWriter(); diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 7d1a0bbfc4..81233f3b2f 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -19,18 +19,18 @@ namespace Avalonia.FreeDesktop return null; var services = await DBusHelper.Connection.ListServicesAsync(); return services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal) - ? new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle) + ? new DBusSystemDialog(DBusHelper.Connection, handle) : null; } - private readonly DesktopService _desktopService; - private readonly FileChooser _fileChooser; + private readonly Connection _connection; + private readonly OrgFreedesktopPortalFileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(DesktopService desktopService, IPlatformHandle handle) + private DBusSystemDialog(Connection connection, IPlatformHandle handle) { - _desktopService = desktopService; - _fileChooser = desktopService.CreateFileChooser("/org/freedesktop/portal/desktop"); + _connection = connection; + _fileChooser = new OrgFreedesktopPortalFileChooser(connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); _handle = handle; } @@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = _desktopService.CreateRequest(objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { @@ -82,7 +82,7 @@ namespace Avalonia.FreeDesktop chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = _desktopService.CreateRequest(objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { @@ -113,7 +113,7 @@ namespace Avalonia.FreeDesktop }; var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = _desktopService.CreateRequest(objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index d6518f7246..215408a0e3 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; @@ -13,14 +11,15 @@ 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 }); private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private readonly DBus? _dBus; + private readonly OrgFreedesktopDBus? _dBus; private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private StatusNotifierWatcher? _statusNotifierWatcher; + private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher; private (int, int, byte[]) _icon; private string? _sysTrayServiceName; @@ -48,7 +47,7 @@ namespace Avalonia.FreeDesktop IsActive = true; - _dBus = new DBusService(_connection, "org.freedesktop.DBus").CreateDBus("/org/freedesktop/DBus"); + _dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); @@ -61,8 +60,7 @@ namespace Avalonia.FreeDesktop if (_connection is null || _isDisposed) return; - _statusNotifierWatcher = new StatusNotifierWatcherService(_connection, "org.kde.StatusNotifierWatcher") - .CreateStatusNotifierWatcher("/StatusNotifierWatcher"); + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); _serviceConnected = true; } @@ -72,7 +70,7 @@ namespace Avalonia.FreeDesktop if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) return; - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.A2) ); + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2) ); var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); OnNameChange(nameOwner); } @@ -147,7 +145,7 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon(StatusNotifierItemDbusObj.StatusNotifierItemProperties.EmptyPixmap); + _statusNotifierItemDbusObj?.SetIcon(EmptyPixmap); return; } @@ -210,27 +208,37 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : IMethodHandler + internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem { private readonly Connection _connection; - private readonly StatusNotifierItemProperties _backingProperties; public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { _connection = connection; - _backingProperties = new StatusNotifierItemProperties - { - Menu = dbusMenuPath, // Needs a dbus menu somehow - ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty) - }; - + BackingProperties.Menu = dbusMenuPath; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); + BackingProperties.IconName = string.Empty; + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.IconThemePath = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; InvalidateAll(); } - public string Path => "/StatusNotifierItem"; + public override string Path => "/StatusNotifierItem"; public event Action? ActivationDelegate; + protected override void OnContextMenu(int x, int y) { } + + protected override void OnActivate(int x, int y) => ActivationDelegate?.Invoke(); + + protected override void OnSecondaryActivate(int x, int y) { } + + protected override void OnScroll(int delta, string orientation) { } + public void InvalidateAll() { EmitVoidSignal("NewTitle"); @@ -238,12 +246,12 @@ namespace Avalonia.FreeDesktop EmitVoidSignal("NewAttentionIcon"); EmitVoidSignal("NewOverlayIcon"); EmitVoidSignal("NewToolTip"); - EmitStringSignal("NewStatus", _backingProperties.Status); + EmitStringSignal("NewStatus", BackingProperties.Status); } public void SetIcon((int, int, byte[]) dbusPixmap) { - _backingProperties.IconPixmap = new[] { dbusPixmap }; + BackingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); } @@ -252,124 +260,14 @@ namespace Avalonia.FreeDesktop if (text is null) return; - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); + BackingProperties.Id = text; + BackingProperties.Category = "ApplicationStatus"; + BackingProperties.Status = text; + BackingProperties.Title = text; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } - public bool RunMethodHandlerSynchronously(Message message) => false; - - public ValueTask HandleMethodAsync(MethodContext context) - { - switch (context.Request.InterfaceAsString) - { - case "org.kde.StatusNotifierItem": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("ContextMenu", "ii"): - break; - case ("Activate", "ii"): - ActivationDelegate?.Invoke(); - break; - case ("SecondaryActivate", "ii"): - break; - case ("Scroll", "is"): - break; - } - - break; - case "org.freedesktop.DBus.Properties": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("Get", "ss"): - { - var reader = context.Request.GetBodyReader(); - _ = reader.ReadString(); - var member = reader.ReadString(); - switch (member) - { - case "Category": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Category); - context.Reply(writer.CreateMessage()); - break; - } - case "Id": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Id); - context.Reply(writer.CreateMessage()); - break; - } - case "Title": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Title); - context.Reply(writer.CreateMessage()); - break; - } - case "Status": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Status); - context.Reply(writer.CreateMessage()); - break; - } - case "Menu": - { - using var writer = context.CreateReplyWriter("o"); - writer.WriteObjectPath(_backingProperties.Menu); - context.Reply(writer.CreateMessage()); - break; - } - case "IconPixmap": - { - using var writer = context.CreateReplyWriter("a(iiay)"); - writer.WriteArray(_backingProperties.IconPixmap); - context.Reply(writer.CreateMessage()); - break; - } - case "ToolTip": - { - using var writer = context.CreateReplyWriter("(sa(iiay)ss)"); - writer.WriteStruct(_backingProperties.ToolTip); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - } - case ("GetAll", "s"): - { - var writer = context.CreateReplyWriter("a{sv}"); - var dict = new Dictionary - { - { "Category", _backingProperties.Category }, - { "Id", _backingProperties.Id }, - { "Title", _backingProperties.Title }, - { "Status", _backingProperties.Status }, - { "Menu", _backingProperties.Menu }, - { "IconPixmap", _backingProperties.IconPixmap }, - { "ToolTip", _backingProperties.ToolTip } - }; - - writer.WriteDictionary(dict); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - } - - return default; - } - private void EmitVoidSignal(string member) { using var writer = _connection.GetMessageWriter(); @@ -384,27 +282,5 @@ namespace Avalonia.FreeDesktop writer.WriteString(value); _connection.TrySendMessage(writer.CreateMessage()); } - - internal record StatusNotifierItemProperties - { - public string Category { get; set; } = string.Empty; - public string Id { get; set; } = string.Empty; - public string Title { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; - public int WindowId { get; set; } - public string? IconThemePath { get; set; } - public ObjectPath Menu { get; set; } - public bool ItemIsMenu { get; set; } - public string? IconName { get; set; } - public (int, int, byte[])[] IconPixmap { get; set; } = { EmptyPixmap }; - public string? OverlayIconName { get; set; } - public (int, int, byte[])[]? OverlayIconPixmap { get; set; } - public string? AttentionIconName { get; set; } - public (int, int, byte[])[]? AttentionIconPixmap { get; set; } - public string? AttentionMovieName { get; set; } - public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } - - public static (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); - } } } diff --git a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs deleted file mode 100644 index 23f1dcf0e7..0000000000 --- a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs +++ /dev/null @@ -1,938 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal record NotificationProperties - { - public uint Version { get; set; } - } - - internal class Notification : DesktopObject - { - private const string Interface = "org.freedesktop.portal.Notification"; - - public Notification(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task AddNotificationAsync(string id, Dictionary notification) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sa{sv}", - member: "AddNotification"); - writer.WriteString(id); - writer.WriteDictionary(notification); - return writer.CreateMessage(); - } - } - - public Task RemoveNotificationAsync(string id) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RemoveNotification"); - writer.WriteString(id); - return writer.CreateMessage(); - } - } - - public ValueTask WatchActionInvokedAsync(Action handler, - bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", static (m, s) - => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static NotificationProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static NotificationProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new NotificationProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal record OpenURIProperties - { - public uint Version { get; set; } - } - - internal class OpenURI : DesktopObject - { - private const string Interface = "org.freedesktop.portal.OpenURI"; - - public OpenURI(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task OpenUriAsync(string parentWindow, string uri, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "OpenURI"); - writer.WriteString(parentWindow); - writer.WriteString(uri); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sha{sv}", - member: "OpenFile"); - writer.WriteString(parentWindow); - writer.WriteHandle(fd); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sha{sv}", - member: "OpenDirectory"); - writer.WriteString(parentWindow); - writer.WriteHandle(fd); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static OpenURIProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static OpenURIProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new OpenURIProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal record DynamicLauncherProperties - { - public uint SupportedLauncherTypes { get; set; } - public uint Version { get; set; } - } - - internal class DynamicLauncher : DesktopObject - { - private const string Interface = "org.freedesktop.portal.DynamicLauncher"; - - public DynamicLauncher(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task InstallAsync(string token, string desktopFileId, string desktopEntry, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sssa{sv}", - member: "Install"); - writer.WriteString(token); - writer.WriteString(desktopFileId); - writer.WriteString(desktopEntry); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssva{sv}", - member: "PrepareInstall"); - writer.WriteString(parentWindow); - writer.WriteString(name); - writer.WriteVariant(iconV); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task RequestInstallTokenAsync(string name, object iconV, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) - => ReadMessage_s(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sva{sv}", - member: "RequestInstallToken"); - writer.WriteString(name); - writer.WriteVariant(iconV); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task UninstallAsync(string desktopFileId, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sa{sv}", - member: "Uninstall"); - writer.WriteString(desktopFileId); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task GetDesktopEntryAsync(string desktopFileId) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) - => ReadMessage_s(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetDesktopEntry"); - writer.WriteString(desktopFileId); - return writer.CreateMessage(); - } - } - - public Task<(object IconV, string IconFormat, uint IconSize)> GetIconAsync(string desktopFileId) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) - => ReadMessage_vsu(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetIcon"); - writer.WriteString(desktopFileId); - return writer.CreateMessage(); - } - } - - public Task LaunchAsync(string desktopFileId, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sa{sv}", - member: "Launch"); - writer.WriteString(desktopFileId); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SetSupportedLauncherTypesAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("SupportedLauncherTypes"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetSupportedLauncherTypesAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static DynamicLauncherProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, - emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "SupportedLauncherTypes": - invalidated.Add("SupportedLauncherTypes"); - break; - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static DynamicLauncherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new DynamicLauncherProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "SupportedLauncherTypes": - reader.ReadSignature("u"); - props.SupportedLauncherTypes = reader.ReadUInt32(); - changedList?.Add("SupportedLauncherTypes"); - break; - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal record FileChooserProperties - { - public uint Version { get; set; } - } - - internal class FileChooser : DesktopObject - { - private const string Interface = "org.freedesktop.portal.FileChooser"; - - public FileChooser(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task OpenFileAsync(string parentWindow, string title, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "OpenFile"); - writer.WriteString(parentWindow); - writer.WriteString(title); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SaveFileAsync(string parentWindow, string title, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "SaveFile"); - writer.WriteString(parentWindow); - writer.WriteString(title); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SaveFilesAsync(string parentWindow, string title, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "SaveFiles"); - writer.WriteString(parentWindow); - writer.WriteString(title); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static FileChooserProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static FileChooserProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new FileChooserProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal class Request : DesktopObject - { - private const string Interface = "org.freedesktop.portal.Request"; - - public Request(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task CloseAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Close"); - return writer.CreateMessage(); - } - } - - public ValueTask WatchResponseAsync(Action results)> handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "Response", - static (m, s) => ReadMessage_uaesv(m, (DesktopObject)s!), handler, emitOnCapturedContext); - } - - internal class DesktopService - { - public DesktopService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public Notification CreateNotification(string path) => new(this, path); - public OpenURI CreateOpenUri(string path) => new(this, path); - public DynamicLauncher CreateDynamicLauncher(string path) => new(this, path); - public FileChooser CreateFileChooser(string path) => new(this, path); - public Request CreateRequest(string path) => new(this, path); - } - - internal class DesktopObject - { - protected DesktopObject(DesktopService service, ObjectPath path) - => (Service, Path) = (service, path); - - protected DesktopService Service { get; } - protected ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) - => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - protected static ObjectPath ReadMessage_o(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadObjectPath(); - } - - protected static uint ReadMessage_v_u(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("u"); - return reader.ReadUInt32(); - } - - protected static (string, string, object[]) ReadMessage_ssav(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadArray(); - return (arg0, arg1, arg2); - } - - protected static (uint, Dictionary) ReadMessage_uaesv(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadDictionary(); - return (arg0, arg1); - } - - protected static string ReadMessage_s(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static (object, string, uint) ReadMessage_vsu(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadUInt32(); - return (arg0, arg1, arg2); - } - } - - internal class PropertyChanges - { - public PropertyChanges(TProperties properties, string[] invalidated, string[] changed) - => (Properties, Invalidated, Changed) = (properties, invalidated, changed); - - public TProperties Properties { get; } - public string[] Invalidated { get; } - public string[] Changed { get; } - public bool HasChanged(string property) => Array.IndexOf(Changed, property) != -1; - public bool IsInvalidated(string property) => Array.IndexOf(Invalidated, property) != -1; - } -} diff --git a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs deleted file mode 100644 index 4e6ce3bd43..0000000000 --- a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal record StatusNotifierWatcherProperties - { - public string[] RegisteredStatusNotifierItems { get; set; } = default!; - public bool IsStatusNotifierHostRegistered { get; set; } - public int ProtocolVersion { get; set; } - } - - internal class StatusNotifierWatcher : StatusNotifierWatcherObject - { - private const string Interface = "org.kde.StatusNotifierWatcher"; - - public StatusNotifierWatcher(StatusNotifierWatcherService service, ObjectPath path) : base(service, path) { } - - public Task RegisterStatusNotifierItemAsync(string service) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RegisterStatusNotifierItem"); - writer.WriteString(service); - return writer.CreateMessage(); - } - } - - public Task RegisterStatusNotifierHostAsync(string service) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RegisterStatusNotifierHost"); - writer.WriteString(service); - return writer.CreateMessage(); - } - } - - public ValueTask WatchStatusNotifierItemRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemRegistered", static (m, s) => - ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchStatusNotifierItemUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemUnregistered", static (m, s) - => ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchStatusNotifierHostRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostRegistered", handler, emitOnCapturedContext); - - public ValueTask WatchStatusNotifierHostUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostUnregistered", handler, emitOnCapturedContext); - - public Task SetRegisteredStatusNotifierItemsAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("RegisteredStatusNotifierItems"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task SetIsStatusNotifierHostRegisteredAsync(bool value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("IsStatusNotifierHostRegistered"); - writer.WriteSignature("b"); - writer.WriteBool(value); - return writer.CreateMessage(); - } - } - - public Task SetProtocolVersionAsync(int value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("ProtocolVersion"); - writer.WriteSignature("i"); - writer.WriteInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetRegisteredStatusNotifierItemsAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "RegisteredStatusNotifierItems"), static (m, s) - => ReadMessage_v_as(m, (StatusNotifierWatcherObject)s!), this); - - public Task GetIsStatusNotifierHostRegisteredAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IsStatusNotifierHostRegistered"), static (m, s) => - ReadMessage_v_b(m, (StatusNotifierWatcherObject)s!), this); - - public Task GetProtocolVersionAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "ProtocolVersion"), static (m, s) - => ReadMessage_v_i(m, (StatusNotifierWatcherObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static StatusNotifierWatcherProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "RegisteredStatusNotifierItems": - invalidated.Add("RegisteredStatusNotifierItems"); - break; - case "IsStatusNotifierHostRegistered": - invalidated.Add("IsStatusNotifierHostRegistered"); - break; - case "ProtocolVersion": - invalidated.Add("ProtocolVersion"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new StatusNotifierWatcherProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "RegisteredStatusNotifierItems": - reader.ReadSignature("as"); - props.RegisteredStatusNotifierItems = reader.ReadArray(); - changedList?.Add("RegisteredStatusNotifierItems"); - break; - case "IsStatusNotifierHostRegistered": - reader.ReadSignature("b"); - props.IsStatusNotifierHostRegistered = reader.ReadBool(); - changedList?.Add("IsStatusNotifierHostRegistered"); - break; - case "ProtocolVersion": - reader.ReadSignature("i"); - props.ProtocolVersion = reader.ReadInt32(); - changedList?.Add("ProtocolVersion"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal class StatusNotifierWatcherService - { - public StatusNotifierWatcherService(Connection connection, string destination) => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - - public StatusNotifierWatcher CreateStatusNotifierWatcher(string path) => new(this, path); - } - - internal class StatusNotifierWatcherObject - { - protected StatusNotifierWatcherObject(StatusNotifierWatcherService service, ObjectPath path) - { - (Service, Path) = (service, path); - } - - protected StatusNotifierWatcherService Service { get; } - protected ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) - => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) - => null!, static (Exception? ex, object _, object? _, object? hs) - => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static string ReadMessage_s(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static string[] ReadMessage_v_as(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("as"); - return reader.ReadArray(); - } - - protected static bool ReadMessage_v_b(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("b"); - return reader.ReadBool(); - } - - protected static int ReadMessage_v_i(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("i"); - return reader.ReadInt32(); - } - } -} diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator new file mode 160000 index 0000000000..1cf4faba30 --- /dev/null +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -0,0 +1 @@ +Subproject commit 1cf4faba30741f799a33313a2842cf70eeb6c67e From ebc77f9ba1dbcc15ab98ae72b4f0f92ef808da42 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:41:42 +0100 Subject: [PATCH 05/36] Add xml files --- src/Avalonia.FreeDesktop/DBusXml/DBus.xml | 89 ++++ src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml | 437 ++++++++++++++++++ .../DBusXml/StatusNotifierItem.xml | 96 ++++ .../DBusXml/StatusNotifierWatcher.xml | 42 ++ .../com.canonical.AppMenu.Registrar.xml | 56 +++ .../DBusXml/org.fcitx.Fcitx.InputContext.xml | 64 +++ .../DBusXml/org.fcitx.Fcitx.InputContext1.xml | 64 +++ .../DBusXml/org.fcitx.Fcitx.InputMethod.xml | 16 + .../DBusXml/org.fcitx.Fcitx.InputMethod1.xml | 12 + .../DBusXml/org.freedesktop.IBus.Portal.xml | 139 ++++++ .../org.freedesktop.portal.FileChooser.xml | 377 +++++++++++++++ .../org.freedesktop.portal.Request.xml | 86 ++++ 12 files changed, 1478 insertions(+) create mode 100644 src/Avalonia.FreeDesktop/DBusXml/DBus.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBus.xml b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml new file mode 100644 index 0000000000..a7ecce70f2 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml new file mode 100644 index 0000000000..de6868cb3e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml @@ -0,0 +1,437 @@ + + + + + + + + Name + Type + Description + Default Value + + + type + String + Can be one of: + - "standard": an item which can be clicked to trigger an action or + show another menu + - "separator": a separator + + Vendor specific types can be added by prefixing them with + "x--". + + "standard" + + + label + string + Text of the item, except that: + -# two consecutive underscore characters "__" are displayed as a + single underscore, + -# any remaining underscore characters are not displayed at all, + -# the first of those remaining underscore characters (unless it is + the last character in the string) indicates that the following + character is the access key. + + "" + + + enabled + boolean + Whether the item can be activated or not. + true + + + visible + boolean + True if the item is visible in the menu. + true + + + icon-name + string + Icon name of the item, following the freedesktop.org icon spec. + "" + + + icon-data + binary + PNG data of the icon. + Empty + + + shortcut + array of arrays of strings + The shortcut of the item. Each array represents the key press + in the list of keypresses. Each list of strings contains a list of + modifiers and then the key that is used. The modifier strings + allowed are: "Control", "Alt", "Shift" and "Super". + + - A simple shortcut like Ctrl+S is represented as: + [["Control", "S"]] + - A complex shortcut like Ctrl+Q, Alt+X is represented as: + [["Control", "Q"], ["Alt", "X"]] + Empty + + + toggle-type + string + + If the item can be toggled, this property should be set to: + - "checkmark": Item is an independent togglable item + - "radio": Item is part of a group where only one item can be + toggled at a time + - "": Item cannot be toggled + + "" + + + toggle-state + int + + Describe the current state of a "togglable" item. Can be one of: + - 0 = off + - 1 = on + - anything else = indeterminate + + Note: + The implementation does not itself handle ensuring that only one + item in a radio group is set to "on", or that a group does not have + "on" and "indeterminate" items simultaneously; maintaining this + policy is up to the toolkit wrappers. + + -1 + + + children-display + string + + If the menu item has children this property should be set to + "submenu". + + "" + + + disposition + string + + How the menuitem feels the information it's displaying to the + user should be presented. + - "normal" a standard menu item + - "informative" providing additional information to the user + - "warning" looking at potentially harmful results + - "alert" something bad could potentially happen + + "normal" + + + + Vendor specific properties can be added by prefixing them with + "x--". + ]]> + + + + + Provides the version of the DBusmenu API that this API is + implementing. + + + + + + Represents the way the text direction of the application. This + allows the server to handle mismatches intelligently. For left- + to-right the string is "ltr" for right-to-left it is "rtl". + + + + + + Tells if the menus are in a normal state or they believe that they + could use some attention. Cases for showing them would be if help + were referring to them or they accessors were being highlighted. + This property can have two values: "normal" in almost all cases and + "notice" when they should have a higher priority to be shown. + + + + + + A list of directories that should be used for finding icons using + the icon naming spec. Idealy there should only be one for the icon + theme, but additional ones are often added by applications for + app specific icons. + + + + + + + + Provides the layout and propertiers that are attached to the entries + that are in the layout. It only gives the items that are children + of the item that is specified in @a parentId. It will return all of the + properties or specific ones depending of the value in @a propertyNames. + + The format is recursive, where the second 'v' is in the same format + as the original 'a(ia{sv}av)'. Its content depends on the value + of @a recursionDepth. + + + The ID of the parent node for the layout. For + grabbing the layout from the root node use zero. + + + + The amount of levels of recursion to use. This affects the + content of the second variant array. + - -1: deliver all the items under the @a parentId. + - 0: no recursion, the array will be empty. + - n: array will contains items up to 'n' level depth. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + The revision number of the layout. For matching + with layoutUpdated signals. + + + The layout, as a recursive structure. + + + + + + Returns the list of items which are children of @a parentId. + + + + A list of ids that we should be finding the properties + on. If the list is empty, all menu items should be sent. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + + An array of property values. + An item in this area is represented as a struct following + this format: + @li id unsigned the item id + @li properties map(string => variant) the requested item properties + + + + + + + Get a signal property on a single item. This is not useful if you're + going to implement this interface, it should only be used if you're + debugging via a commandline tool. + + + the id of the item which received the event + + + the name of the property to get + + + the value of the property + + + + + -" + ]]> + + the id of the item which received the event + + + the type of event + + + event-specific data + + + The time that the event occured if available or the time the message was sent if not + + + + + + Used to pass a set of events as a single message for possibily several + different menuitems. This is done to optimize DBus traffic. + + + + An array of all the events that should be passed. This tuple should + match the parameters of the 'Event' signal. Which is roughly: + id, eventID, data and timestamp. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + This is called by the applet to notify the application that it is about + to show the menu under the specified item. + + + + Which menu item represents the parent of the item about to be shown. + + + + + Whether this AboutToShow event should result in the menu being updated. + + + + + + + A function to tell several menus being shown that they are about to + be shown to the user. This is likely only useful for programitc purposes + so while the return values are returned, in general, the singular function + should be used in most user interacation scenarios. + + + + The IDs of the menu items who's submenus are being shown. + + + + + The IDs of the menus that need updates. Note: if no update information + is needed the DBus message should set the no reply flag. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + + Triggered when there are lots of property updates across many items + so they all get grouped into a single dbus message. The format is + the ID of the item with a hashtable of names and values for those + properties. + + + + + + + Triggered by the application to notify display of a layout update, up to + revision + + + The revision of the layout that we're currently on + + + + If the layout update is only of a subtree, this is the + parent item for the entries that have changed. It is zero if + the whole layout should be considered invalid. + + + + + + The server is requesting that all clients displaying this + menu open it to the user. This would be for things like + hotkeys that when the user presses them the menu should + open and display itself to the user. + + + ID of the menu that should be activated + + + The time that the event occured + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml new file mode 100644 index 0000000000..7866a74639 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml new file mode 100644 index 0000000000..2eb1a7a0b8 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml new file mode 100644 index 0000000000..42a71707b6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml @@ -0,0 +1,56 @@ + + + + + + An interface to register a menu from an application's window to be displayed in another + window.  This manages that association between XWindow Window IDs and the dbus + address and object that provides the menu using the dbusmenu dbus interface. + + + + + The XWindow ID of the window + + + The object on the dbus interface implementing the dbusmenu interface + + + + + A method to allow removing a window from the database. Windows will also be removed + when the client drops off DBus so this is not required. It is polite though. And + important for testing. + + + The XWindow ID of the window + + + + Gets the registered menu for a given window ID. + + The XWindow ID of the window to get + + + The address of the connection on DBus (e.g. :1.23 or org.example.service) + + + The path to the object which implements the com.canonical.dbusmenu interface. + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml new file mode 100644 index 0000000000..b30d94cebf --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml new file mode 100644 index 0000000000..6cb130d48a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml new file mode 100644 index 0000000000..b8d60f0d37 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml new file mode 100644 index 0000000000..0cc358a09a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml new file mode 100644 index 0000000000..376ad424d4 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml new file mode 100644 index 0000000000..2ae3546955 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml new file mode 100644 index 0000000000..c1abb4eb7b --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + From 18737db8f4b0f486067069724e74b7b86b0bc8a7 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 9 Jan 2023 18:12:32 +0100 Subject: [PATCH 06/36] Use generated signal emit methods --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 33 +++++++------------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 33 ++++++-------------- src/tools/Tmds.DBus.SourceGenerator | 2 +- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 77f5048cd3..cb6def056f 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using Avalonia.Reactive; -using System.Threading.Tasks; +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -27,7 +26,6 @@ namespace Avalonia.FreeDesktop private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { - private readonly Connection _connection; private readonly Dictionary _idsToItems = new(); private readonly Dictionary _itemsToIds = new(); private readonly HashSet _menus = new(); @@ -42,7 +40,7 @@ namespace Avalonia.FreeDesktop public DBusMenuExporterImpl(Connection connection, IntPtr xid) { - _connection = connection; + Connection = connection; _xid = (uint)xid.ToInt32(); Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); @@ -51,13 +49,15 @@ namespace Avalonia.FreeDesktop public DBusMenuExporterImpl(Connection connection, string path) { - _connection = connection; + Connection = connection; _appMenu = false; Path = path; SetNativeMenu(new NativeMenu()); Init(); } + protected override Connection Connection { get; } + public override string Path { get; } protected override (uint revision, (int, Dictionary, object[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) @@ -79,12 +79,12 @@ namespace Avalonia.FreeDesktop protected override object OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? 0; protected override void OnEvent(int id, string eventId, object data, uint timestamp) => - Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId)); protected override int[] OnEventGroup((int, string, object, uint)[] events) { foreach (var e in events) - Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2, e.Item3, e.Item4)); + Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2)); return Array.Empty(); } @@ -95,13 +95,13 @@ namespace Avalonia.FreeDesktop private async void Init() { - _connection.AddMethodHandler(this); + Connection.AddMethodHandler(this); if (!_appMenu) return; - var services = await _connection.ListServicesAsync(); + var services = await Connection.ListServicesAsync(); if (!services.Contains("com.canonical.AppMenu.Registrar")) return; - _registrar = new ComCanonicalAppMenuRegistrar(_connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); + _registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); if (!_disposed) await _registrar.RegisterWindowAsync(_xid, Path); // It's not really important if this code succeeds, @@ -155,7 +155,7 @@ namespace Avalonia.FreeDesktop _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; - EmitUIntIntSignal("LayoutUpdated", _revision, 0); + EmitLayoutUpdated(_revision, 0); } private void QueueReset() @@ -318,7 +318,7 @@ namespace Avalonia.FreeDesktop } - private void HandleEvent(int id, string eventId, object data, uint timestamp) + private void HandleEvent(int id, string eventId) { if (eventId == "clicked") { @@ -327,15 +327,6 @@ namespace Avalonia.FreeDesktop bridge.RaiseClicked(); } } - - private void EmitUIntIntSignal(string member, uint arg0, int arg1) - { - using var writer = _connection.GetMessageWriter(); - writer.WriteSignalHeader(null, Path, "com.canonical.dbusmenu", member, "ui"); - writer.WriteUInt32(arg0); - writer.WriteInt32(arg1); - _connection.TrySendMessage(writer.CreateMessage()); - } } } } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 4e9c495665..16b1a1d686 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -211,11 +211,9 @@ namespace Avalonia.FreeDesktop /// internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem { - private readonly Connection _connection; - public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { - _connection = connection; + Connection = connection; BackingProperties.Menu = dbusMenuPath; BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); BackingProperties.IconName = string.Empty; @@ -228,6 +226,8 @@ namespace Avalonia.FreeDesktop InvalidateAll(); } + protected override Connection Connection { get; } + public override string Path => "/StatusNotifierItem"; public event Action? ActivationDelegate; @@ -242,12 +242,12 @@ namespace Avalonia.FreeDesktop public void InvalidateAll() { - EmitVoidSignal("NewTitle"); - EmitVoidSignal("NewIcon"); - EmitVoidSignal("NewAttentionIcon"); - EmitVoidSignal("NewOverlayIcon"); - EmitVoidSignal("NewToolTip"); - EmitStringSignal("NewStatus", BackingProperties.Status); + EmitNewTitle(); + EmitNewIcon(); + EmitNewAttentionIcon(); + EmitNewOverlayIcon(); + EmitNewToolTip(); + EmitNewStatus(BackingProperties.Status); } public void SetIcon((int, int, byte[]) dbusPixmap) @@ -268,20 +268,5 @@ namespace Avalonia.FreeDesktop BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } - - private void EmitVoidSignal(string member) - { - using var writer = _connection.GetMessageWriter(); - writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member); - _connection.TrySendMessage(writer.CreateMessage()); - } - - private void EmitStringSignal(string member, string value) - { - using var writer = _connection.GetMessageWriter(); - writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member, "s"); - writer.WriteString(value); - _connection.TrySendMessage(writer.CreateMessage()); - } } } diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator index 1cf4faba30..6007f27d04 160000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -1 +1 @@ -Subproject commit 1cf4faba30741f799a33313a2842cf70eeb6c67e +Subproject commit 6007f27d04691f7c4c35722df3dc65bf4b558f18 From b570bfa1a9f0fad0af88f109788a8af05256f040 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:12:09 +0100 Subject: [PATCH 07/36] Fix ObjectDisposedException on Dispose --- .../DBusIme/DBusTextInputMethodBase.cs | 7 +++++-- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 8 +++++++- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 6 ++++++ src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 1 - 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 08d2a0c219..9fd58402c6 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -60,7 +60,7 @@ namespace Avalonia.FreeDesktop.DBusIme { foreach (var name in _knownNames) { - var dbus = new OrgFreedesktopDBus(Connection, name, "/org/freedesktop/DBus"); + var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); var nameOwner = await dbus.GetNameOwnerAsync(name); OnNameChange(null, (name, null, nameOwner)); @@ -74,6 +74,9 @@ namespace Avalonia.FreeDesktop.DBusIme private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args) { + if (e is not null) + return; + if (args.NewOwner is not null && _currentName is null) { _onlineNamesQueue.Enqueue(args.ServiceName); @@ -162,7 +165,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected void AddDisposable(IDisposable? d) { - if(d is { }) + if (d is { }) _disposables.Add(d); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 6b30c5424a..be88b94707 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -134,6 +134,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(Exception? e, string s) => FireCommit(s); + private void OnCommitString(Exception? e, string s) + { + if (e is not null) + return; + + FireCommit(s); + } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 4db0d95d3c..57bb10a885 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -28,6 +28,9 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { + if (e is not null) + return; + var state = (IBusModifierMask)k.state; KeyModifiers mods = default; if (state.HasAllFlags(IBusModifierMask.ControlMask)) @@ -48,6 +51,9 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnCommitText(Exception? e, object wtf) { + if (e is not null) + return; + // Hello darkness, my old friend if (wtf.GetType().GetField("Item3") is { } prop) { diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 16b1a1d686..a34a9a0a84 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using Avalonia.Reactive; using System.Linq; using Avalonia.Controls.Platform; using Avalonia.Logging; From 86b61050a0ec49fdefe225c79e01ea67af4deb0a Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 19 Jan 2023 19:47:35 +0100 Subject: [PATCH 08/36] Use DBusVariantItem to manually encode/decode variants Note: Tray-Icon submenus don't work, needs investigation --- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 13 +-- src/Avalonia.FreeDesktop/DBusInterfaces.cs | 3 +- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 72 +++++++------- .../DBusPlatformSettings.cs | 85 ++++++++-------- src/Avalonia.FreeDesktop/DBusSettings.cs | 16 --- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 74 +++++++------- .../org.freedesktop.portal.Settings.xml | 99 +++++++++++++++++++ src/Linux/Tmds.DBus | 2 +- src/tools/Tmds.DBus.SourceGenerator | 2 +- 9 files changed, 226 insertions(+), 140 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/DBusSettings.cs create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 57bb10a885..59e9ecd1cf 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -4,6 +4,8 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; + namespace Avalonia.FreeDesktop.DBusIme.IBus { @@ -49,18 +51,13 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus }); } - private void OnCommitText(Exception? e, object wtf) + private void OnCommitText(Exception? e, DBusVariantItem variantItem) { if (e is not null) return; - // Hello darkness, my old friend - if (wtf.GetType().GetField("Item3") is { } prop) - { - var text = prop.GetValue(wtf) as string; - if (!string.IsNullOrEmpty(text)) - FireCommit(text!); - } + if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem) + FireCommit(stringItem.Value); } protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; diff --git a/src/Avalonia.FreeDesktop/DBusInterfaces.cs b/src/Avalonia.FreeDesktop/DBusInterfaces.cs index 3bece27ab1..fb2f0ca2f3 100644 --- a/src/Avalonia.FreeDesktop/DBusInterfaces.cs +++ b/src/Avalonia.FreeDesktop/DBusInterfaces.cs @@ -9,9 +9,10 @@ namespace Avalonia.FreeDesktop [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod.xml")] [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext1.xml")] [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod1.xml")] + [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] [DBusInterface("./DBusXml/org.freedesktop.portal.FileChooser.xml")] [DBusInterface("./DBusXml/org.freedesktop.portal.Request.xml")] - [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] + [DBusInterface("./DBusXml/org.freedesktop.portal.Settings.xml")] [DBusHandler("./DBusXml/DBusMenu.xml")] [DBusHandler("./DBusXml/StatusNotifierItem.xml")] internal class DBusInterfaces { } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index cb6def056f..1978dfba81 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -9,8 +9,7 @@ using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; using Tmds.DBus.Protocol; - -#pragma warning disable 1998 +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { @@ -60,7 +59,7 @@ namespace Avalonia.FreeDesktop public override string Path { get; } - protected override (uint revision, (int, Dictionary, object[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) + protected override (uint revision, (int, Dictionary, DBusVariantItem[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) { var menu = GetMenu(parentId); var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); @@ -73,15 +72,15 @@ namespace Avalonia.FreeDesktop return (_revision, layout); } - protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => + protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray(); - protected override object OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? 0; + protected override DBusVariantItem OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? new DBusVariantItem("i", new DBusInt32Item(0)); - protected override void OnEvent(int id, string eventId, object data, uint timestamp) => + protected override void OnEvent(int id, string eventId, DBusVariantItem data, uint timestamp) => Dispatcher.UIThread.Post(() => HandleEvent(id, eventId)); - protected override int[] OnEventGroup((int, string, object, uint)[] events) + protected override int[] OnEventGroup((int, string, DBusVariantItem, uint)[] events) { foreach (var e in events) Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2)); @@ -115,14 +114,14 @@ namespace Avalonia.FreeDesktop if (_disposed) return; _disposed = true; - // Fire and forget - _registrar?.UnregisterWindowAsync(_xid); + _ = _registrar?.UnregisterWindowAsync(_xid); } public bool IsNativeMenuExported { get; private set; } + public event EventHandler? OnIsNativeMenuExportedChanged; public void SetNativeMenu(NativeMenu? menu) @@ -203,31 +202,31 @@ namespace Avalonia.FreeDesktop QueueReset(); } - private static readonly string[] AllProperties = { + private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - private object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + private DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; if (it is NativeMenuItemSeparator) { if (name == "type") - return "separator"; + return new DBusVariantItem("s", new DBusStringItem("separator")); } else if (it is NativeMenuItem item) { if (name == "type") return null; if (name == "label") - return item.Header ?? ""; + return new DBusVariantItem("s", new DBusStringItem(item.Header ?? "")); if (name == "enabled") { if (item.Menu is not null && item.Menu.Items.Count == 0) - return false; + return new DBusVariantItem("b", new DBusBoolItem(false)); if (!item.IsEnabled) - return false; + return new DBusVariantItem("b", new DBusBoolItem(false)); return null; } if (name == "shortcut") @@ -236,30 +235,30 @@ namespace Avalonia.FreeDesktop return null; if (item.Gesture.KeyModifiers == 0) return null; - var lst = new List(); + var lst = new List(); var mod = item.Gesture; if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - lst.Add("Control"); + lst.Add(new DBusStringItem("Control")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - lst.Add("Alt"); + lst.Add(new DBusStringItem("Alt")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - lst.Add("Shift"); + lst.Add(new DBusStringItem("Shift")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - lst.Add("Super"); - lst.Add(item.Gesture.Key.ToString()); - return new[] { lst.ToArray() }; + 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) })); } if (name == "toggle-type") { if (item.ToggleType == NativeMenuItemToggleType.CheckBox) - return "checkmark"; + return new DBusVariantItem("s", new DBusStringItem("checkmark")); if (item.ToggleType == NativeMenuItemToggleType.Radio) - return "radio"; + return new DBusVariantItem("s", new DBusStringItem("radio")); } if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) - return item.IsChecked ? 1 : 0; + return new DBusVariantItem("i", new DBusInt32Item(item.IsChecked ? 1 : 0)); if (name == "icon-data") { @@ -273,23 +272,24 @@ namespace Avalonia.FreeDesktop using var ms = new MemoryStream(); icon.Save(ms); - return ms.ToArray(); + return new DBusVariantItem("ay", + new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x)))); } } } if (name == "children-display") - return menu is not null ? "submenu" : null; + return menu is not null ? new DBusVariantItem("s", new DBusStringItem("submenu")) : null; } return null; } - private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names.Length == 0) - names = AllProperties; - var properties = new Dictionary(); + names = s_allProperties; + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); @@ -300,17 +300,23 @@ namespace Avalonia.FreeDesktop return properties; } - private (int, Dictionary, object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) + private (int, Dictionary, DBusVariantItem[]) 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() : new object[menu.Items.Count]; + var children = depth == 0 || menu is null ? Array.Empty() : new DBusVariantItem[menu.Items.Count]; if (menu is not null) { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; - children[c] = 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[] + { + new DBusInt32Item(layout.Item1), + new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))), + new DBusArrayItem(DBusType.Variant, layout.Item3) + })); } } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 039fc7c088..a25bb68458 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -2,44 +2,35 @@ using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform; +using Tmds.DBus.SourceGenerator; -namespace Avalonia.FreeDesktop; - -internal class DBusPlatformSettings : DefaultPlatformSettings +namespace Avalonia.FreeDesktop { - private readonly IDBusSettings? _settings; - private PlatformColorValues? _lastColorValues; - - public DBusPlatformSettings() + internal class DBusPlatformSettings : DefaultPlatformSettings { - _settings = DBusHelper.TryInitialize()? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; - if (_settings is not null) + public DBusPlatformSettings() { - _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + if (DBusHelper.Connection is null) + return; - _ = TryGetInitialValue(); + _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + _ = TryGetInitialValueAsync(); } - } - - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } - private async Task TryGetInitialValue() - { - var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - if (colorSchemeTask.Status == TaskStatus.RanToCompletion) + public override PlatformColorValues GetColorValues() { - _lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result); + return _lastColorValues ?? base.GetColorValues(); } - else + + private async Task TryGetInitialValueAsync() { try { - var value = await colorSchemeTask; + var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); _lastColorValues = GetColorValuesFromSetting(value); OnColorValuesChanged(_lastColorValues); } @@ -49,29 +40,31 @@ internal class DBusPlatformSettings : DefaultPlatformSettings Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); } } - } - - private void SettingsChangedHandler((string @namespace, string key, object value) tuple) - { - if (tuple.@namespace == "org.freedesktop.appearance" - && tuple.key == "color-scheme") + + private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(tuple.value); - OnColorValuesChanged(_lastColorValues); + if (exception is not null) + return; + + if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _lastColorValues = GetColorValuesFromSetting(value); + OnColorValuesChanged(_lastColorValues); + } } - } - - private static PlatformColorValues GetColorValuesFromSetting(object value) - { - var isDark = value?.ToString() == "1"; - return new PlatformColorValues + + private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; + return new PlatformColorValues + { + ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; + } } } diff --git a/src/Avalonia.FreeDesktop/DBusSettings.cs b/src/Avalonia.FreeDesktop/DBusSettings.cs deleted file mode 100644 index 05911981c7..0000000000 --- a/src/Avalonia.FreeDesktop/DBusSettings.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus; - -namespace Avalonia.FreeDesktop; - -[DBusInterface("org.freedesktop.portal.Settings")] -internal interface IDBusSettings : IDBusObject -{ - Task<(string @namespace, IDictionary)> ReadAllAsync(string[] namespaces); - - Task ReadAsync(string @namespace, string key); - - Task WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action? onError = null); -} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 81233f3b2f..763480a33d 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { @@ -44,12 +44,12 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeFilter); - if (filters.Any()) + if (filters is not null) chooserOptions.Add("filters", filters); - chooserOptions.Add("multiple", options.AllowMultiple); + chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple))); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); @@ -58,9 +58,8 @@ namespace Avalonia.FreeDesktop using var disposable = await request.WatchResponseAsync((e, x) => { if (e is not null) - tsc.TrySetException(e); - else - tsc.TrySetResult(x.results["uris"] as string[]); + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); }); var uris = await tsc.Task ?? Array.Empty(); @@ -71,15 +70,15 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeChoices); - if (filters.Any()) + if (filters is not null) chooserOptions.Add("filters", filters); if (options.SuggestedFileName is { } currentName) - chooserOptions.Add("current_name", currentName); + chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true) - chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); + chooserOptions.Add("current_folder", new DBusVariantItem("s", new DBusStringItem(currentFolder.ToString()))); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); @@ -87,9 +86,8 @@ namespace Avalonia.FreeDesktop using var disposable = await request.WatchResponseAsync((e, x) => { if (e is not null) - tsc.TrySetException(e); - else - tsc.TrySetResult(x.results["uris"] as string[]); + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); }); var uris = await tsc.Task; @@ -106,10 +104,10 @@ namespace Avalonia.FreeDesktop public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { var parentWindow = $"x11:{_handle.Handle:X}"; - var chooserOptions = new Dictionary + var chooserOptions = new Dictionary { - { "directory", true }, - { "multiple", options.AllowMultiple } + { "directory", new DBusVariantItem("b", new DBusBoolItem(true)) }, + { "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) } }; var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); @@ -118,9 +116,8 @@ namespace Avalonia.FreeDesktop using var disposable = await request.WatchResponseAsync((e, x) => { if (e is not null) - tsc.TrySetException(e); - else - tsc.TrySetResult(x.results["uris"] as string[]); + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); }); var uris = await tsc.Task ?? Array.Empty(); @@ -131,29 +128,38 @@ namespace Avalonia.FreeDesktop .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } - private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList? fileTypes) + private static DBusVariantItem? ParseFilters(IReadOnlyList? fileTypes) { + const uint GlobStyle = 0u; + const uint MimeStyle = 1u; + // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] if (fileTypes is null) - return Array.Empty<(string name, (uint style, string extension)[])>(); + return null; + + var any = false; + var filters = new DBusArrayItem(DBusType.Struct, new List()); - var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) { - const uint GlobStyle = 0u; - const uint MimeStyle = 1u; - - var extensions = Enumerable.Empty<(uint, string)>(); + var extensions = new List(); + if (fileType.Patterns?.Count > 0) + extensions.AddRange( + fileType.Patterns.Select(static pattern => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) }))); + else if (fileType.MimeTypes?.Count > 0) + extensions.AddRange( + fileType.MimeTypes.Select(static mimeType => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) }))); + else + continue; - if (fileType.Patterns is not null) - extensions = extensions.Concat(fileType.Patterns.Select(static x => (globStyle: GlobStyle, x))); - else if (fileType.MimeTypes is not null) - extensions = extensions.Concat(fileType.MimeTypes.Select(static x => (mimeStyle: MimeStyle, x))); - if (extensions.Any()) - filters.Add((fileType.Name, extensions.ToArray())); + any = true; + filters.Add(new DBusStructItem( + new DBusItem[] { new DBusStringItem(fileType.Name), new DBusArrayItem(DBusType.Struct, extensions) })); } - return filters.ToArray(); + return any ? new DBusVariantItem("a(sa(us))", filters) : null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml new file mode 100644 index 0000000000..669997a3df --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Linux/Tmds.DBus b/src/Linux/Tmds.DBus index bfca94ab05..e86bcf1bc2 160000 --- a/src/Linux/Tmds.DBus +++ b/src/Linux/Tmds.DBus @@ -1 +1 @@ -Subproject commit bfca94ab052683c7ccef51e4a036098f539cc676 +Subproject commit e86bcf1bc2d86338ab61663a4ae48dbc0bd7e02d diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator index 6007f27d04..86e0ded07f 160000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -1 +1 @@ -Subproject commit 6007f27d04691f7c4c35722df3dc65bf4b558f18 +Subproject commit 86e0ded07fc8622e216f93f8fc01e8c1b3ef29b1 From a4665747e3f6aa3f31784d6adddf358e1b84f8a5 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 19 Jan 2023 23:31:41 +0100 Subject: [PATCH 09/36] Don't dispose DBus connection in tray icon --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 7 ++++--- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 1978dfba81..6f1810d1c1 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -43,7 +44,7 @@ namespace Avalonia.FreeDesktop _xid = (uint)xid.ToInt32(); Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } public DBusMenuExporterImpl(Connection connection, string path) @@ -52,7 +53,7 @@ namespace Avalonia.FreeDesktop _appMenu = false; Path = path; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } protected override Connection Connection { get; } @@ -92,7 +93,7 @@ namespace Avalonia.FreeDesktop protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) => (Array.Empty(), Array.Empty()); - private async void Init() + private async Task InitializeAsync() { Connection.AddMethodHandler(this); if (!_appMenu) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index a34a9a0a84..f732cfef07 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -134,7 +134,6 @@ namespace Avalonia.FreeDesktop IsActive = false; _isDisposed = true; DestroyTrayIcon(); - _connection?.Dispose(); _serviceWatchDisposable?.Dispose(); } From 042edf132b94c32d31b67c64c7bdae88d379c7bb Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:07:51 +0100 Subject: [PATCH 10/36] Remove unnecessary ItemGroup --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index e1e0a636ea..a77de9bb6a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -24,8 +24,4 @@ - - - - From 4baed8b569e4f613c066e4fd67013e2a8d863c33 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:12:55 +0100 Subject: [PATCH 11/36] Delete DBusMenu.cs --- src/Avalonia.FreeDesktop/DBusMenu.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/DBusMenu.cs diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs deleted file mode 100644 index e69de29bb2..0000000000 From c978887dd6e47e089e272718a2de404872392de3 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:13:06 +0100 Subject: [PATCH 12/36] Make GetProperty static --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 6f1810d1c1..deac7346cb 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -207,7 +207,7 @@ namespace Avalonia.FreeDesktop "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - private DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + private static DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; From 9a26fef972a8d02328b26574927c0e338407fabb Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:19:48 +0100 Subject: [PATCH 13/36] Remove NetAnalyzers.props import --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a77de9bb6a..c0f82a6e67 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -6,7 +6,6 @@ - From f2050c868ac6ac4d0d134e99304b5c681f5b808d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 1 Feb 2023 01:22:16 -0500 Subject: [PATCH 14/36] Extract theme related properties to IThemeVariantHost and move property definitions to ThemeVariantScope --- .../Controls/ResourceNodeExtensions.cs | 39 ++++++++----------- src/Avalonia.Base/StyledElement.cs | 33 ---------------- .../Styling/IGlobalThemeVariantProvider.cs | 22 ----------- .../Styling/IThemeVariantHost.cs | 26 +++++++++++++ src/Avalonia.Controls/Application.cs | 19 ++++----- src/Avalonia.Controls/Control.cs | 17 +++++++- src/Avalonia.Controls/ThemeVariantScope.cs | 17 ++++++++ src/Avalonia.Controls/TopLevel.cs | 14 +++++-- .../Diagnostics/Controls/Application.cs | 2 +- .../StaticResourceExtension.cs | 2 +- .../Xaml/ThemeDictionariesTests.cs | 4 +- 11 files changed, 99 insertions(+), 96 deletions(-) delete mode 100644 src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs create mode 100644 src/Avalonia.Base/Styling/IThemeVariantHost.cs diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 4b0bab0c92..8aed1545a5 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -138,18 +138,18 @@ namespace Avalonia.Controls protected override void Initialize() { _target.ResourcesChanged += ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } } protected override void Deinitialize() { _target.ResourcesChanged -= ResourcesChanged; - if (_target is StyledElement themeStyleable) + if (_target is IThemeVariantHost themeVariantHost) { - themeStyleable.PropertyChanged -= PropertyChanged; + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; } } @@ -163,18 +163,15 @@ namespace Avalonia.Controls PublishNext(GetValue()); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(GetValue()); - } + PublishNext(GetValue()); } private object? GetValue() { - if (_target is not StyledElement themeStyleable - || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (_target is not IThemeVariantHost themeVariantHost + || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } @@ -236,9 +233,9 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is StyledElement styleable) + if (_owner is IThemeVariantHost themeVariantHost) { - styleable.PropertyChanged += PropertyChanged; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } _owner = _target.Owner; @@ -247,20 +244,18 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is StyledElement styleable2) + if (_owner is IThemeVariantHost themeVariantHost2) { - styleable2.PropertyChanged += PropertyChanged; + themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } + PublishNext(); } - private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + private void ActualThemeVariantChanged(object? sender, EventArgs e) { - if (e.Property == StyledElement.ActualThemeVariantProperty) - { - PublishNext(); - } + PublishNext(); } private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) @@ -270,8 +265,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is StyledElement themeStyleable) - || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value)) + if (!(_target.Owner is IThemeVariantHost themeVariantHost) + || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) { value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; } diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 5bf022cd51..17d0699336 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -71,23 +71,6 @@ namespace Avalonia public static readonly StyledProperty ThemeProperty = AvaloniaProperty.Register(nameof(Theme)); - /// - /// Defines the property. - /// - public static readonly StyledProperty ActualThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); - - /// - /// Defines the RequestedThemeVariant property. - /// - public static readonly StyledProperty RequestedThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ThemeVariant), - defaultValue: ThemeVariant.Default); - private static readonly ControlTheme s_invalidTheme = new ControlTheme(); private int _initCount; private string? _name; @@ -274,15 +257,6 @@ namespace Avalonia set => SetValue(ThemeProperty, value); } - /// - /// Gets the UI theme that is currently used by the element, which might be different than the . - /// - /// - /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. - /// Otherwise, current OS theme variant is returned. - /// - public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); - /// /// Gets the styled element's logical children. /// @@ -647,13 +621,6 @@ namespace Avalonia if (change.Property == ThemeProperty) OnControlThemeChanged(); - else if (change.Property == RequestedThemeVariantProperty) - { - if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) - SetValue(ActualThemeVariantProperty, themeVariant); - else - ClearValue(ActualThemeVariantProperty); - } } private protected virtual void OnControlThemeChanged() diff --git a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs b/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs deleted file mode 100644 index 2467d99b3b..0000000000 --- a/src/Avalonia.Base/Styling/IGlobalThemeVariantProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Metadata; - -namespace Avalonia.Styling; - -/// -/// Interface for an application host element with a root theme variant. -/// -[Unstable] -public interface IGlobalThemeVariantProvider : IResourceHost -{ - /// - /// Gets the UI theme variant that is used by the control (and its child elements) for resource determination. - /// - ThemeVariant ActualThemeVariant { get; } - - /// - /// Raised when the theme variant is changed on the element or an ancestor of the element. - /// - event EventHandler? ActualThemeVariantChanged; -} diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs new file mode 100644 index 0000000000..01583148a8 --- /dev/null +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls; +using Avalonia.Metadata; + +namespace Avalonia.Styling; + +/// +/// Interface for the host element with a theme variant. +/// +[Unstable] +public interface IThemeVariantHost : IResourceHost +{ + /// + /// Gets the UI theme that is currently used by the element, which might be different than the RequestedThemeVariantProperty. + /// + /// + /// If current control is contained in the ThemeVariantScope, TopLevel or Application with non-default RequestedThemeVariant, that value will be returned. + /// Otherwise, current OS theme variant is returned. + /// + ThemeVariant ActualThemeVariant { get; } + + /// + /// Raised when the theme variant is changed on the element or an ancestor of the element. + /// + event EventHandler? ActualThemeVariantChanged; +} diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 6d3ba3cf8a..8456a9a3a9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -29,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IGlobalThemeVariantProvider, IApplicationPlatformEvents + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IThemeVariantHost, IApplicationPlatformEvents { /// /// The application-global data templates. @@ -50,13 +50,13 @@ namespace Avalonia public static readonly StyledProperty DataContextProperty = StyledElement.DataContextProperty.AddOwner(); - /// + /// public static readonly StyledProperty ActualThemeVariantProperty = - StyledElement.ActualThemeVariantProperty.AddOwner(); + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); - /// + /// public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); /// public event EventHandler? ResourcesChanged; @@ -95,11 +95,8 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// - public ThemeVariant ActualThemeVariant - { - get => GetValue(ActualThemeVariantProperty); - } + /// + public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// /// Gets the current instance of the class. @@ -256,7 +253,7 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this) .Bind().ToConstant(this) - .Bind().ToConstant(this) + .Bind().ToConstant(this) .Bind().ToConstant(FocusManager) .Bind().ToConstant(InputManager) .Bind().ToTransient() diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index ed24c3c7c2..bfef190dfd 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls /// - A property to allow user-defined data to be attached to the control. /// - and other context menu related members. /// - public class Control : InputElement, IDataTemplateHost, INamed, IVisualBrushInitialize, ISetterValue + public class Control : InputElement, IDataTemplateHost, INamed, IVisualBrushInitialize, ISetterValue, IThemeVariantHost { /// /// Defines the property. @@ -163,6 +163,10 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } + + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariantScope.ActualThemeVariantProperty); + + public event EventHandler? ActualThemeVariantChanged; /// /// Occurs when the user has completed a context input gesture, such as a right-click. @@ -530,6 +534,17 @@ namespace Avalonia.Controls RaiseEvent(sizeChangedEventArgs); } } + else if (change.Property == ThemeVariantScope.RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ThemeVariantScope.ActualThemeVariantProperty, themeVariant); + else + ClearValue(ThemeVariantScope.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariantScope.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } } } } diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index b9724251c7..2aac1d3e2f 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -7,6 +7,23 @@ namespace Avalonia.Controls /// public class ThemeVariantScope : Decorator { + /// + /// Defines the property. + /// + public static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + inherits: true, + defaultValue: ThemeVariant.Light); + + /// + /// Defines the property. + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + nameof(ThemeVariant), + defaultValue: ThemeVariant.Default); + /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 676fa1519a..f9f7cfc4c6 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -79,6 +79,14 @@ namespace Avalonia.Controls public static readonly StyledProperty TransparencyBackgroundFallbackProperty = AvaloniaProperty.Register(nameof(TransparencyBackgroundFallback), Brushes.White); + /// + public static readonly StyledProperty ActualThemeVariantProperty = + ThemeVariantScope.ActualThemeVariantProperty.AddOwner(); + + /// + public static readonly StyledProperty RequestedThemeVariantProperty = + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); + /// /// Defines the event. /// @@ -96,7 +104,7 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; - private readonly IGlobalThemeVariantProvider? _applicationThemeHost; + private readonly IThemeVariantHost? _applicationThemeHost; private readonly PointerOverPreProcessor? _pointerOverPreProcessor; private readonly IDisposable? _pointerOverPreProcessorSubscription; private readonly IDisposable? _backGestureSubscription; @@ -153,7 +161,7 @@ namespace Avalonia.Controls _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderInterface = TryGetService(dependencyResolver); _globalStyles = TryGetService(dependencyResolver); - _applicationThemeHost = TryGetService(dependencyResolver); + _applicationThemeHost = TryGetService(dependencyResolver); Renderer = impl.CreateRenderer(this); @@ -633,7 +641,7 @@ namespace Avalonia.Controls private void GlobalActualThemeVariantChanged(object? sender, EventArgs e) { - SetValue(ActualThemeVariantProperty, ((IGlobalThemeVariantProvider)sender!).ActualThemeVariant, BindingPriority.Template); + SetValue(ActualThemeVariantProperty, ((IThemeVariantHost)sender!).ActualThemeVariant, BindingPriority.Template); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 7426c4e2ed..e21983700d 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -14,7 +14,7 @@ namespace Avalonia.Diagnostics.Controls public event EventHandler? Closed; public static readonly StyledProperty RequestedThemeVariantProperty = - StyledElement.RequestedThemeVariantProperty.AddOwner(); + ThemeVariantScope.RequestedThemeVariantProperty.AddOwner(); public Application(Avalonia.Application application) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index cdd344becc..bef32766dc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -34,7 +34,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var stack = serviceProvider.GetService(); var provideTarget = serviceProvider.GetService(); - var themeVariant = (provideTarget.TargetObject as StyledElement)?.ActualThemeVariant; + var themeVariant = (provideTarget.TargetObject as IThemeVariantHost)?.ActualThemeVariant; var targetType = provideTarget.TargetProperty switch { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index c5b62cdff2..20d76a3cc7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -185,9 +185,9 @@ public class ThemeDictionariesTests : XamlTestBase { using (AvaloniaLocator.EnterScope()) { - var applicationThemeHost = new Mock(); + var applicationThemeHost = new Mock(); applicationThemeHost.SetupGet(h => h.ActualThemeVariant).Returns(ThemeVariant.Dark); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(applicationThemeHost.Object); var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" Date: Wed, 1 Feb 2023 01:39:11 -0500 Subject: [PATCH 15/36] Fix DataGrid theming --- .../Themes/Fluent.xaml | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index dd8575c989..2197b0fe59 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -1,6 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.6 0.8 @@ -9,19 +51,6 @@ M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z - - - - - - - - - - - @@ -29,23 +58,10 @@ - + - - - - - - - - - @@ -565,5 +581,6 @@ + From 0c20abec120cd9a0039342c88737c4b0085d6e13 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 1 Feb 2023 02:27:27 -0500 Subject: [PATCH 16/36] Update src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml --- src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index 2197b0fe59..ca516c8918 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -22,7 +22,7 @@ - + From dff80c8ded5cd28bb6b0f3535aee27f6394c1f4e Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 5 Feb 2023 12:55:46 +0100 Subject: [PATCH 17/36] Remove ListServicesAsync usage - Uses Reflection underneath - It's faster to just fail than receiving all available services first --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 25 ++++++++++++-------- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 24 ++++++++++++------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 24 +++++++++++-------- src/tools/Tmds.DBus.SourceGenerator | 2 +- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index deac7346cb..d86a6bf3a4 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -98,16 +98,21 @@ namespace Avalonia.FreeDesktop Connection.AddMethodHandler(this); if (!_appMenu) return; - var services = await Connection.ListServicesAsync(); - if (!services.Contains("com.canonical.AppMenu.Registrar")) - return; + _registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); - if (!_disposed) - await _registrar.RegisterWindowAsync(_xid, Path); - // It's not really important if this code succeeds, - // and it's not important to know if it succeeds - // since even if we register the window it's not guaranteed that - // menu will be actually exported + try + { + if (!_disposed) + await _registrar.RegisterWindowAsync(_xid, Path); + } + catch + { + // It's not really important if this code succeeds, + // and it's not important to know if it succeeds + // since even if we register the window it's not guaranteed that + // menu will be actually exported + _registrar = null; + } } public void Dispose() @@ -286,7 +291,7 @@ namespace Avalonia.FreeDesktop return null; } - private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names.Length == 0) names = s_allProperties; diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 1c965a077e..fa36f7d1ab 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -17,20 +17,28 @@ namespace Avalonia.FreeDesktop { if (DBusHelper.Connection is null) return null; - var services = await DBusHelper.Connection.ListServicesAsync(); - return services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal) - ? new DBusSystemDialog(DBusHelper.Connection, handle) - : null; + + var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + try + { + await dbusFileChooser.GetVersionAsync(); + } + catch + { + return null; + } + + return new DBusSystemDialog(DBusHelper.Connection, handle, dbusFileChooser); } private readonly Connection _connection; private readonly OrgFreedesktopPortalFileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(Connection connection, IPlatformHandle handle) + private DBusSystemDialog(Connection connection, IPlatformHandle handle, OrgFreedesktopPortalFileChooser fileChooser) { _connection = connection; - _fileChooser = new OrgFreedesktopPortalFileChooser(connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _fileChooser = fileChooser; _handle = handle; } @@ -137,7 +145,6 @@ namespace Avalonia.FreeDesktop if (fileTypes is null) return null; - var any = false; var filters = new DBusArrayItem(DBusType.Struct, new List()); foreach (var fileType in fileTypes) @@ -154,7 +161,6 @@ namespace Avalonia.FreeDesktop else continue; - any = true; filters.Add(new DBusStructItem( new DBusItem[] { @@ -163,7 +169,7 @@ namespace Avalonia.FreeDesktop })); } - return any ? new DBusVariantItem("a(sa(us))", filters) : null; + return filters.Count > 0 ? new DBusVariantItem("a(sa(us))", filters) : null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index f732cfef07..521fe68e90 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Linq; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; @@ -66,13 +65,18 @@ namespace Avalonia.FreeDesktop private async void WatchAsync() { - var services = await _connection!.ListServicesAsync(); - if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) - return; - - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2) ); - var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); - OnNameChange(nameOwner); + try + { + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2)); + var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); + OnNameChange(nameOwner); + } + catch + { + _serviceWatchDisposable = null; + Logger.TryGet(LogEventLevel.Error, "DBUS") + ?.Log(this, "Interface 'org.kde.StatusNotifierWatcher' is unavailable."); + } } private void OnNameChange(string? newOwner) @@ -99,7 +103,7 @@ namespace Avalonia.FreeDesktop private async void CreateTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null) return; #if NET5_0_OR_GREATER @@ -114,7 +118,7 @@ namespace Avalonia.FreeDesktop _connection.AddMethodHandler(_statusNotifierItemDbusObj); await _dBus!.RequestNameAsync(_sysTrayServiceName, 0); - await _statusNotifierWatcher!.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator index 86e0ded07f..309159609e 160000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -1 +1 @@ -Subproject commit 86e0ded07fc8622e216f93f8fc01e8c1b3ef29b1 +Subproject commit 309159609e09f3f0ae77f0abd761360049761700 From 04df472194e194559d33619e089f3aa918e3f5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Thu, 9 Feb 2023 03:10:19 +0000 Subject: [PATCH 18/36] Added unit test for #10226. --- .../Styling/StyledElementTests_Theming.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index b5a9b35134..60603937d9 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -104,7 +104,7 @@ public class StyledElementTests_Theming target.Theme = null; Assert.Equal("style", target.Tag); } - + [Fact] public void TemplatedParent_Theme_Is_Detached_From_Template_Controls_When_Theme_Property_Cleared() { @@ -539,12 +539,42 @@ public class StyledElementTests_Theming Assert.Same(target.Theme, theme3); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_To_Children() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + Assert.Equal(Brushes.Red, border.Background); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); } - private static TestRoot CreateRoot(Control child) + private static TestRoot CreateRoot(Control child, ControlTheme? theme = null) { var result = new TestRoot() { @@ -552,7 +582,7 @@ public class StyledElementTests_Theming { new Style(x => x.OfType()) { - Setters = { new Setter(StyledElement.ThemeProperty, CreateTheme()) } + Setters = { new Setter(StyledElement.ThemeProperty, theme ?? CreateTheme()) } } } }; @@ -580,8 +610,8 @@ public class StyledElementTests_Theming { new Style(x => x.Nesting().Template().OfType()) { - Setters = - { + Setters = + { new Setter(Border.BackgroundProperty, Brushes.Red), new Setter(Control.TagProperty, tag), } From b8a987e84748b3c26d37dbfa787e720c0a188aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 8 Feb 2023 20:29:25 +0000 Subject: [PATCH 19/36] Ensure templated parent control theme is applied. --- src/Avalonia.Base/Layout/Layoutable.cs | 6 ++++++ src/Avalonia.Base/StyledElement.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..c45fc16929 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -795,6 +795,12 @@ namespace Avalonia.Layout InvalidateMeasure(); } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + InvalidateMeasure(); + } + /// /// Called when the layout manager raises a LayoutUpdated event. /// diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 5bf022cd51..b910012444 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -390,7 +390,7 @@ namespace Avalonia /// public bool ApplyStyling() { - if (_initCount == 0 && (!_stylesApplied || !_themeApplied)) + if (_initCount == 0 && (!_stylesApplied || !_themeApplied || !_templatedParentThemeApplied)) { GetValueStore().BeginStyling(); From 3ace482c198a56f558cb1369b5885508d2c839da Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 20:32:12 -0500 Subject: [PATCH 20/36] Fix wrong property names and documentation --- src/Avalonia.Base/Styling/ThemeVariant.cs | 2 +- src/Avalonia.Controls/Application.cs | 2 +- src/Avalonia.Controls/Control.cs | 1 + src/Avalonia.Controls/ThemeVariantScope.cs | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 8218533f4f..5a6a8785a5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Styling; /// -/// Specifies a UI theme variant that should be used for the +/// Specifies a UI theme variant that should be used for the Control and Application types. /// [TypeConverter(typeof(ThemeVariantTypeConverter))] public sealed record ThemeVariant diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 8456a9a3a9..6d9a6bd493 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -95,7 +95,7 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// + /// public ThemeVariant ActualThemeVariant => GetValue(ActualThemeVariantProperty); /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 775fc8e243..c9e8f0d045 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -162,6 +162,7 @@ namespace Avalonia.Controls set => SetValue(TagProperty, value); } + /// public ThemeVariant ActualThemeVariant => GetValue(ThemeVariantScope.ActualThemeVariantProperty); public event EventHandler? ActualThemeVariantChanged; diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index 2aac1d3e2f..ff950d3985 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -8,11 +8,11 @@ namespace Avalonia.Controls public class ThemeVariantScope : Decorator { /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty ActualThemeVariantProperty = AvaloniaProperty.Register( - nameof(ThemeVariant), + nameof(ActualThemeVariant), inherits: true, defaultValue: ThemeVariant.Light); @@ -21,9 +21,9 @@ namespace Avalonia.Controls /// public static readonly StyledProperty RequestedThemeVariantProperty = AvaloniaProperty.Register( - nameof(ThemeVariant), + nameof(RequestedThemeVariant), defaultValue: ThemeVariant.Default); - + /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination. /// The UI theme you specify with ThemeVariant can override the app-level ThemeVariant. From af70f74c88163b9e0d5446163685ec9f0b748e7d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 23:03:11 -0500 Subject: [PATCH 21/36] Workaround for the inherited default value bug --- src/Avalonia.Controls/ThemeVariantScope.cs | 3 +- .../Xaml/ThemeDictionariesTests.cs | 73 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index ff950d3985..b29f1ee56e 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -13,8 +13,7 @@ namespace Avalonia.Controls public static readonly StyledProperty ActualThemeVariantProperty = AvaloniaProperty.Register( nameof(ActualThemeVariant), - inherits: true, - defaultValue: ThemeVariant.Light); + inherits: true); /// /// Defines the property. diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 20d76a3cc7..3ac4677694 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -1,8 +1,12 @@ -using Avalonia.Controls; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using Avalonia.Styling; +using Avalonia.UnitTests; using Moq; using Xunit; @@ -444,4 +448,71 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } + + [Fact] + public void Theme_Switch_Works_In_Nested_Scope() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + +"); + window.ApplyTemplate(); + + var scope = window.FindControl("Scope")!; + var text = window.FindControl("Text")!; + + Assert.Equal(ThemeVariant.Dark, text.ActualThemeVariant); + Assert.Equal(Color.Parse("#dedede"), ((ISolidColorBrush)text.Foreground!).Color); + + scope.RequestedThemeVariant = ThemeVariant.Light; + Assert.Equal(ThemeVariant.Light, text.ActualThemeVariant); + Assert.Equal(Colors.Black, ((ISolidColorBrush)text.Foreground!).Color); + } + } + + [Fact] + public void Theme_Switch_Works_In_With_Popup() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + + +"); + window.Show(); + + var scope = window.FindControl("Scope")!; + var popup = window.FindControl("Popup")!; + + popup.IsOpen = true; + + var border = (Border)popup.Child!; + + Assert.Equal(ThemeVariant.Dark, popup.ActualThemeVariant); + Assert.Equal(ThemeVariant.Dark, border.ActualThemeVariant); + Assert.Equal(Color.Parse("#282828"), ((ISolidColorBrush)border.Background!).Color); + + scope.RequestedThemeVariant = ThemeVariant.Light; + + Assert.Equal(ThemeVariant.Light, popup.ActualThemeVariant); + Assert.Equal(ThemeVariant.Light, border.ActualThemeVariant); + Assert.Equal(Colors.White, ((ISolidColorBrush)border.Background!).Color); + } + } + } } From 9933fd04ce136e0376b78b57355556267b28493e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 23:47:55 -0500 Subject: [PATCH 22/36] Bind target theme variant to the popup root --- src/Avalonia.Controls/Primitives/Popup.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d6cd71aedc..142db71761 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -387,6 +387,15 @@ namespace Avalonia.Controls.Primitives popupHost.Transform = null; } + if (popupHost is PopupRoot topLevelPopup) + { + topLevelPopup + .Bind( + ThemeVariantScope.ActualThemeVariantProperty, + placementTarget.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + .DisposeWith(handlerCleanup); + } + UpdateHostPosition(popupHost, placementTarget); SubscribeToEventHandler>(popupHost, RootTemplateApplied, From 5d0d3ba5bbe791590215a88d8e05b83ab68f9ae4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 15 Feb 2023 00:05:03 -0500 Subject: [PATCH 23/36] Simplify simple themes of ported control --- .../Controls/SplitButton.xaml | 104 +++++++----------- .../Controls/SplitView.xaml | 6 +- .../Controls/ToggleSwitch.xaml | 91 +++++---------- 3 files changed, 73 insertions(+), 128 deletions(-) diff --git a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml index 3c621a981d..2a7dc081f0 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitButton.xaml @@ -2,8 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="using:Avalonia.Controls.Converters"> - + @@ -26,37 +25,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -75,66 +44,75 @@ - - - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SplitView.xaml b/src/Avalonia.Themes.Simple/Controls/SplitView.xaml index d6f293a730..f839e9a598 100644 --- a/src/Avalonia.Themes.Simple/Controls/SplitView.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SplitView.xaml @@ -17,8 +17,6 @@ 320 48 - 00:00:00.2 00:00:00.1 0.1,0.9,0.2,1.0 @@ -240,7 +238,9 @@ From 104023bfc88a9e7ee6e7033c4181494f302b6e61 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 18 Feb 2023 00:27:21 -0500 Subject: [PATCH 24/36] Remove specific data type methods from the IDataObject, add new Files format --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 41 ++++++++------- .../ControlCatalog/Pages/DragAndDropPage.xaml | 3 +- .../Pages/DragAndDropPage.xaml.cs | 48 +++++++++++++++--- src/Avalonia.Base/Input/DataFormats.cs | 10 +++- src/Avalonia.Base/Input/DataObject.cs | 25 ++++------ .../Input/DataObjectExtensions.cs | 50 +++++++++++++++++++ src/Avalonia.Base/Input/IDataObject.cs | 17 ++----- .../Platform/Storage/FileIO/BclStorageFile.cs | 5 -- .../Storage/FileIO/BclStorageFolder.cs | 9 ---- .../Storage/FileIO/StorageProviderHelpers.cs | 17 +++++++ .../Platform/Storage/PickerOptions.cs | 2 + .../Storage/StorageProviderExtensions.cs | 12 +++++ src/Avalonia.Native/ClipboardImpl.cs | 41 +++++++++------ .../Avalonia.Win32/ClipboardFormats.cs | 3 ++ src/Windows/Avalonia.Win32/DataObject.cs | 15 ++---- src/Windows/Avalonia.Win32/OleDataObject.cs | 18 +++---- 16 files changed, 213 insertions(+), 103 deletions(-) create mode 100644 src/Avalonia.Base/Input/DataObjectExtensions.cs diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index e24860e3e1..e5f29abb68 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -306,25 +306,8 @@ namespace ControlCatalog.Pages resultText += @$" Content: "; -#if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); -#else - using var stream = await file.OpenReadAsync(); -#endif - using var reader = new System.IO.StreamReader(stream); - // 4GB file test, shouldn't load more than 10000 chars into a memory. - const int length = 10000; - var buffer = ArrayPool.Shared.Rent(length); - try - { - var charsRead = await reader.ReadAsync(buffer, 0, length); - resultText += new string(buffer, 0, charsRead); - } - finally - { - ArrayPool.Shared.Return(buffer); - } + resultText += await ReadTextFromFile(file, 10000); } openedFileContent.Text = resultText; @@ -354,6 +337,28 @@ namespace ControlCatalog.Pages } } + public static async Task ReadTextFromFile(IStorageFile file, int length) + { +#if NET6_0_OR_GREATER + await using var stream = await file.OpenReadAsync(); +#else + using var stream = await file.OpenReadAsync(); +#endif + using var reader = new System.IO.StreamReader(stream); + + // 4GB file test, shouldn't load more than 10000 chars into a memory. + var buffer = ArrayPool.Shared.Rent(length); + try + { + var charsRead = await reader.ReadAsync(buffer, 0, length); + return new string(buffer, 0, charsRead); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 3f8a023060..390fa32b9c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -25,7 +25,6 @@ BorderThickness="2"> Drag Me (custom) - + + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index e384db88b3..26430b4b61 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -1,27 +1,29 @@ using System; +using System.IO; using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Platform.Storage; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - TextBlock _DropState; + private readonly TextBlock _dropState; private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); - _DropState = this.Get("DropState"); + _dropState = this.Get("DropState"); int textCount = 0; SetupDnd("Text", d => d.Set(DataFormats.Text, $"Text was dragged {++textCount} times"), DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link); SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"), DragDropEffects.Move); - SetupDnd("Files", d => d.Set(DataFormats.FileNames, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); + SetupDnd("Files", d => d.Set(DataFormats.Files, new[] { Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }), DragDropEffects.Copy); } void SetupDnd(string suffix, Action factory, DragDropEffects effects) @@ -68,12 +70,12 @@ namespace ControlCatalog.Pages // Only allow if the dragged data contains text or filenames. if (!e.Data.Contains(DataFormats.Text) - && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(DataFormats.Files) && !e.Data.Contains(CustomFormat)) e.DragEffects = DragDropEffects.None; } - void Drop(object? sender, DragEventArgs e) + async void Drop(object? sender, DragEventArgs e) { if (e.Source is Control c && c.Name == "MoveTarget") { @@ -85,11 +87,41 @@ namespace ControlCatalog.Pages } if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); + { + _dropState.Text = e.Data.GetText(); + } + else if (e.Data.Contains(DataFormats.Files)) + { + var files = e.Data.GetFiles() ?? Array.Empty(); + var contentStr = ""; + + foreach (var item in files) + { + if (item is IStorageFile file) + { + var content = await DialogsPage.ReadTextFromFile(file, 1000); + contentStr += $"File {item.Name}:{Environment.NewLine}{content}{Environment.NewLine}{Environment.NewLine}"; + } + else if (item is IStorageFolder folder) + { + var items = await folder.GetItemsAsync(); + contentStr += $"Folder {item.Name}: items {items.Count}{Environment.NewLine}{Environment.NewLine}"; + } + } + + _dropState.Text = contentStr; + } +#pragma warning disable CS0618 // Type or member is obsolete else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames() ?? Array.Empty()); + { + var files = e.Data.GetFileNames(); + _dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty()); + } +#pragma warning restore CS0618 // Type or member is obsolete else if (e.Data.Contains(CustomFormat)) - _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + { + _dropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } } dragMe.PointerPressed += DoDrag; diff --git a/src/Avalonia.Base/Input/DataFormats.cs b/src/Avalonia.Base/Input/DataFormats.cs index cf5a6592e1..35d50e669a 100644 --- a/src/Avalonia.Base/Input/DataFormats.cs +++ b/src/Avalonia.Base/Input/DataFormats.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Input +using System; + +namespace Avalonia.Input { public static class DataFormats { @@ -7,9 +9,15 @@ /// public static readonly string Text = nameof(Text); + /// + /// Dataformat for one or more files. + /// + public static readonly string Files = nameof(Files); + /// /// Dataformat for one or more filenames /// + [Obsolete("Use DataFormats.Files, this format is supported only on desktop platforms.")] public static readonly string FileNames = nameof(FileNames); } } diff --git a/src/Avalonia.Base/Input/DataObject.cs b/src/Avalonia.Base/Input/DataObject.cs index 688f5f9cc8..93a6baa03c 100644 --- a/src/Avalonia.Base/Input/DataObject.cs +++ b/src/Avalonia.Base/Input/DataObject.cs @@ -2,37 +2,34 @@ namespace Avalonia.Input { + /// + /// Specific and mutable implementation of the IDataObject interface. + /// public class DataObject : IDataObject { - private readonly Dictionary _items = new Dictionary(); + private readonly Dictionary _items = new(); + /// public bool Contains(string dataFormat) { return _items.ContainsKey(dataFormat); } + /// public object? Get(string dataFormat) { - if (_items.ContainsKey(dataFormat)) - return _items[dataFormat]; - return null; + return _items.TryGetValue(dataFormat, out var item) ? item : null; } + /// public IEnumerable GetDataFormats() { return _items.Keys; } - public IEnumerable? GetFileNames() - { - return Get(DataFormats.FileNames) as IEnumerable; - } - - public string? GetText() - { - return Get(DataFormats.Text) as string; - } - + /// + /// Sets a value to the internal store of the data object with as a key. + /// public void Set(string dataFormat, object value) { _items[dataFormat] = value; diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs new file mode 100644 index 0000000000..807c242914 --- /dev/null +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; + +namespace Avalonia.Input +{ + public static class DataObjectExtensions + { + /// + /// Returns a list of files if the DataObject contains files or filenames. + /// . + /// + /// + /// Collection of storage items - files or folders. If format isn't avaialble, returns null. + /// + public static IEnumerable? GetFiles(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Files) as IEnumerable; + } + + /// + /// Returns a list of filenames if the DataObject contains filenames. + /// + /// + /// + /// Collection of file names. If format isn't avaialble, returns null. + /// + [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] + public static IEnumerable? GetFileNames(this IDataObject dataObject) + { + return (dataObject.Get(DataFormats.FileNames) as IEnumerable) + ?? dataObject.GetFiles()? + .Select(f => f.TryGetLocalPath()) + .Where(p => !string.IsNullOrEmpty(p)) + .OfType(); + } + + /// + /// Returns the dragged text if the DataObject contains any text. + /// + /// + /// + /// A text string. If format isn't avaialble, returns null. + /// + public static string? GetText(this IDataObject dataObject) + { + return dataObject.Get(DataFormats.Text) as string; + } + } +} diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs index 1db008aa3a..b6fcd8c7db 100644 --- a/src/Avalonia.Base/Input/IDataObject.cs +++ b/src/Avalonia.Base/Input/IDataObject.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform.Storage; namespace Avalonia.Input { @@ -19,21 +21,12 @@ namespace Avalonia.Input /// bool Contains(string dataFormat); - /// - /// Returns the dragged text if the DataObject contains any text. - /// - /// - string? GetText(); - - /// - /// Returns a list of filenames if the DataObject contains filenames. - /// - /// - IEnumerable? GetFileNames(); - /// /// Tries to get the data of the given DataFormat. /// + /// + /// Object data. If format isn't avaialble, returns null. + /// object? Get(string dataFormat); } } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index 5bf9ff9d9a..543fb0ab74 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFile : IStorageBookmarkFile { - public BclStorageFile(string fileName) - { - FileInfo = new FileInfo(fileName); - } - public BclStorageFile(FileInfo fileInfo) { FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs index 1e21c197bb..d8e3d91f75 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs @@ -9,15 +9,6 @@ namespace Avalonia.Platform.Storage.FileIO; internal class BclStorageFolder : IStorageBookmarkFolder { - public BclStorageFolder(string path) - { - DirectoryInfo = new DirectoryInfo(path); - if (!DirectoryInfo.Exists) - { - throw new ArgumentException("Directory must exist"); - } - } - public BclStorageFolder(DirectoryInfo directoryInfo) { DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs index 55e84ee937..a8cbffb417 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs @@ -7,6 +7,23 @@ namespace Avalonia.Platform.Storage.FileIO; internal static class StorageProviderHelpers { + public static IStorageItem? TryCreateBclStorageItem(string path) + { + var directory = new DirectoryInfo(path); + if (directory.Exists) + { + return new BclStorageFolder(directory); + } + + var file = new FileInfo(path); + if (file.Exists) + { + return new BclStorageFile(file); + } + + return null; + } + public static Uri FilePathToUri(string path) { var uriPath = new StringBuilder(path) diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs index 6f97916a26..ed061aa2d5 100644 --- a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs @@ -12,6 +12,8 @@ public class PickerOptions /// /// Gets or sets the initial location where the file open picker looks for files to present to the user. + /// Can be obtained from previously picked folder or using + /// or . /// public IStorageFolder? SuggestedStartLocation { get; set; } } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index 6f8b945cd6..1febb4506a 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -11,12 +11,24 @@ public static class StorageProviderExtensions /// public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(filePath) as IStorageFile); + } + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } /// public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { + // We can avoid double escaping of the path by checking for BclStorageProvider. + if (provider is BclStorageProvider) + { + return Task.FromResult(StorageProviderHelpers.TryCreateBclStorageItem(folderPath) as IStorageFolder); + } + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 9f1c8883aa..5a6b0df801 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Runtime.InteropServices; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Native.Interop; -using Avalonia.Platform.Interop; +using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Native { @@ -56,8 +56,13 @@ namespace Avalonia.Native { if(fmt.String == NSPasteboardTypeString) rv.Add(DataFormats.Text); - if(fmt.String == NSFilenamesPboardType) - rv.Add(DataFormats.FileNames); + if (fmt.String == NSFilenamesPboardType) + { +#pragma warning disable CS0618 // Type or member is obsolete + rv.Add(DataFormats.FileNames); +#pragma warning restore CS0618 // Type or member is obsolete + rv.Add(DataFormats.Files); + } } } } @@ -74,7 +79,13 @@ namespace Avalonia.Native public IEnumerable GetFileNames() { using (var strings = _native.GetStrings(NSFilenamesPboardType)) - return strings.ToStringArray(); + return strings?.ToStringArray(); + } + + public IEnumerable GetFiles() + { + return GetFileNames()?.Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!) + .Where(f => f is not null); } public unsafe Task SetDataObjectAsync(IDataObject data) @@ -102,8 +113,12 @@ namespace Avalonia.Native { if (format == DataFormats.Text) return await GetTextAsync(); +#pragma warning disable CS0618 // Type or member is obsolete if (format == DataFormats.FileNames) return GetFileNames(); +#pragma warning restore CS0618 // Type or member is obsolete + if (format == DataFormats.Files) + return GetFiles(); using (var n = _native.GetBytes(format)) return n.Bytes; } @@ -131,20 +146,16 @@ namespace Avalonia.Native public bool Contains(string dataFormat) => Formats.Contains(dataFormat); - public string GetText() - { - // bad idea in general, but API is synchronous anyway - return _clipboard.GetTextAsync().Result; - } - - public IEnumerable GetFileNames() => _clipboard.GetFileNames(); - public object Get(string dataFormat) { if (dataFormat == DataFormats.Text) - return GetText(); + return _clipboard.GetTextAsync().Result; + if (dataFormat == DataFormats.Files) + return _clipboard.GetFiles(); +#pragma warning disable CS0618 if (dataFormat == DataFormats.FileNames) - return GetFileNames(); +#pragma warning restore CS0618 + return _clipboard.GetFileNames(); return null; } } diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index 5fc4f21b2e..00fdeb2a1d 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -29,7 +29,10 @@ namespace Avalonia.Win32 private static readonly List s_formatList = new() { new ClipboardFormat(DataFormats.Text, (ushort)UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, (ushort)UnmanagedMethods.ClipboardFormat.CF_TEXT), + new ClipboardFormat(DataFormats.Files, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), +#pragma warning disable CS0618 // Type or member is obsolete new ClipboardFormat(DataFormats.FileNames, (ushort)UnmanagedMethods.ClipboardFormat.CF_HDROP), +#pragma warning restore CS0618 // Type or member is obsolete }; diff --git a/src/Windows/Avalonia.Win32/DataObject.cs b/src/Windows/Avalonia.Win32/DataObject.cs index 272300cbf3..a215a0a322 100644 --- a/src/Windows/Avalonia.Win32/DataObject.cs +++ b/src/Windows/Avalonia.Win32/DataObject.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; using Avalonia.MicroCom; +using Avalonia.Platform.Storage; using Avalonia.Win32.Interop; using FORMATETC = Avalonia.Win32.Interop.FORMATETC; @@ -124,16 +125,6 @@ namespace Avalonia.Win32 return _wrapped.GetDataFormats(); } - IEnumerable? IDataObject.GetFileNames() - { - return _wrapped.GetFileNames(); - } - - string? IDataObject.GetText() - { - return _wrapped.GetText(); - } - object? IDataObject.Get(string dataFormat) { return _wrapped.Get(dataFormat); @@ -260,8 +251,12 @@ namespace Avalonia.Win32 object data = _wrapped.Get(dataFormat)!; if (dataFormat == DataFormats.Text || data is string) return WriteStringToHGlobal(ref hGlobal, Convert.ToString(data) ?? string.Empty); +#pragma warning disable CS0618 // Type or member is obsolete if (dataFormat == DataFormats.FileNames && data is IEnumerable files) return WriteFileListToHGlobal(ref hGlobal, files); +#pragma warning restore CS0618 // Type or member is obsolete + if (dataFormat == DataFormats.Files && data is IEnumerable items) + return WriteFileListToHGlobal(ref hGlobal, items.Select(f => f.TryGetLocalPath()).Where(f => f is not null)!); if (data is Stream stream) { var length = (int)(stream.Length - stream.Position); diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index 247d0340c3..824303b7fa 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; using Avalonia.Input; +using Avalonia.Platform.Storage.FileIO; using Avalonia.Utilities; using Avalonia.Win32.Interop; using MicroCom.Runtime; @@ -34,16 +35,6 @@ namespace Avalonia.Win32 return GetDataFormatsCore().Distinct(); } - public string? GetText() - { - return (string?)GetDataFromOleHGLOBAL(DataFormats.Text, DVASPECT.DVASPECT_CONTENT); - } - - public IEnumerable? GetFileNames() - { - return (IEnumerable?)GetDataFromOleHGLOBAL(DataFormats.FileNames, DVASPECT.DVASPECT_CONTENT); - } - public object? Get(string dataFormat) { return GetDataFromOleHGLOBAL(dataFormat, DVASPECT.DVASPECT_CONTENT); @@ -67,8 +58,15 @@ namespace Avalonia.Win32 { if (format == DataFormats.Text) return ReadStringFromHGlobal(medium.unionmember); +#pragma warning disable CS0618 if (format == DataFormats.FileNames) +#pragma warning restore CS0618 return ReadFileNamesFromHGlobal(medium.unionmember); + if (format == DataFormats.Files) + return ReadFileNamesFromHGlobal(medium.unionmember) + .Select(f => StorageProviderHelpers.TryCreateBclStorageItem(f)!) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + .Where(f => f is not null); byte[] data = ReadBytesFromHGlobal(medium.unionmember); From dcb73b9fefee2fa5a6509fe1764590a175a6aa36 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 18 Feb 2023 00:27:40 -0500 Subject: [PATCH 25/36] Browser drag n drop target support --- src/Browser/Avalonia.Browser/AvaloniaView.cs | 55 +++++++++++ .../Avalonia.Browser/BrowserDataObject.cs | 91 +++++++++++++++++++ .../Avalonia.Browser/BrowserTopLevelImpl.cs | 9 ++ src/Browser/Avalonia.Browser/ClipboardImpl.cs | 2 +- .../Interop/AvaloniaModule.cs | 21 +++-- .../Interop/GeneralHelpers.cs | 22 +++++ .../Avalonia.Browser/Interop/InputHelper.cs | 5 +- .../Avalonia.Browser/Interop/StorageHelper.cs | 3 + .../Storage/BrowserStorageProvider.cs | 16 ++-- .../webapp/modules/avalonia.ts | 4 +- .../webapp/modules/avalonia/generalHelpers.ts | 19 ++++ .../webapp/modules/avalonia/input.ts | 22 +++++ .../webapp/modules/storage/storageItem.ts | 42 ++++++++- .../webapp/modules/storage/storageProvider.ts | 8 +- 14 files changed, 289 insertions(+), 30 deletions(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserDataObject.cs create mode 100644 src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs create mode 100644 src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 3bb7260e55..76947c949c 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -106,6 +106,8 @@ namespace Avalonia.Browser InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnPointerCancel, OnWheel); + InputHelper.SubscribeDropEvents(_containerElement, OnDragEvent); + var skiaOptions = AvaloniaLocator.Current.GetService(); _dpi = DomHelper.ObserveDpi(OnDpiChanged); @@ -293,6 +295,59 @@ namespace Avalonia.Browser return modifiers; } + public bool OnDragEvent(JSObject args) + { + var eventType = args?.GetPropertyAsString("type") switch + { + "dragenter" => RawDragEventType.DragEnter, + "dragover" => RawDragEventType.DragOver, + "dragleave" => RawDragEventType.DragLeave, + "drop" => RawDragEventType.Drop, + _ => (RawDragEventType)(int)-1 + }; + var dataObject = args?.GetPropertyAsJSObject("dataTransfer"); + if (args is null || eventType < 0 || dataObject is null) + { + return false; + } + + // If file is dropped, we need storage js to be referenced. + // TODO: restructure JS files, so it's not needed. + _ = AvaloniaModule.ImportStorage(); + + var position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")); + var modifiers = GetModifiers(args); + + var effectAllowedStr = dataObject.GetPropertyAsString("effectAllowed") ?? "none"; + var effectAllowed = DragDropEffects.None; + if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Copy; + } + if (effectAllowedStr.Contains("link", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Link; + } + if (effectAllowedStr.Contains("move", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Move; + } + if (effectAllowedStr.Equals("all", StringComparison.OrdinalIgnoreCase)) + { + effectAllowed |= DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link; + } + if (effectAllowed == DragDropEffects.None) + { + return false; + } + + var dropEffect = _topLevelImpl.RawDragEvent(eventType, position, modifiers, new BrowserDataObject(dataObject), effectAllowed); + dataObject.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant()); + + return eventType is RawDragEventType.Drop or RawDragEventType.DragOver + && dropEffect != DragDropEffects.None; + } + private bool OnKeyDown (string code, string key, int modifier) { var handled = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); diff --git a/src/Browser/Avalonia.Browser/BrowserDataObject.cs b/src/Browser/Avalonia.Browser/BrowserDataObject.cs new file mode 100644 index 0000000000..f1e30ee3fe --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserDataObject.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using Avalonia.Browser.Interop; +using Avalonia.Browser.Storage; +using Avalonia.Input; +using Avalonia.Platform.Storage; + +namespace Avalonia.Browser; + +internal class BrowserDataObject : IDataObject +{ + private readonly JSObject _dataObject; + + public BrowserDataObject(JSObject dataObject) + { + _dataObject = dataObject; + } + + public IEnumerable GetDataFormats() + { + var types = new HashSet(_dataObject.GetPropertyAsStringArray("types")); + var dataFormats = new HashSet(types.Count); + + foreach (var type in types) + { + if (type.StartsWith("text/", StringComparison.Ordinal)) + { + dataFormats.Add(DataFormats.Text); + } + else if (type.Equals("Files", StringComparison.Ordinal)) + { + dataFormats.Add(DataFormats.Files); + } + dataFormats.Add(type); + } + + // If drag'n'drop an image from the another web page, if won't add "Files" to the supported types, but only a "text/uri-list". + // With "text/uri-list" browser can add actual file as well. + var filesCount = _dataObject.GetPropertyAsJSObject("files")?.GetPropertyAsInt32("count"); + if (filesCount > 0) + { + dataFormats.Add(DataFormats.Files); + } + + return dataFormats; + } + + public bool Contains(string dataFormat) + { + return GetDataFormats().Contains(dataFormat); + } + + public object? Get(string dataFormat) + { + if (dataFormat == DataFormats.Files) + { + var files = _dataObject.GetPropertyAsJSObject("files"); + if (files is not null) + { + return StorageHelper.FilesToItemsArray(files) + .Select(reference => reference.GetPropertyAsString("kind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } + + return null; + } + + if (dataFormat == DataFormats.Text) + { + if (_dataObject.CallMethodString("getData", "text/plain") is { Length :> 0 } textData) + { + return textData; + } + } + + if (_dataObject.CallMethodString("getData", dataFormat) is { Length: > 0 } data) + { + return data; + } + + return null; + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index f1cd441f45..1bf4636f61 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -164,6 +164,15 @@ namespace Avalonia.Browser return false; } + + public DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers, BrowserDataObject dataObject, DragDropEffects dropEffect) + { + var device = AvaloniaLocator.Current.GetRequiredService(); + var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers); + Console.WriteLine($"{eventArgs.Location} {eventArgs.Effects} {eventArgs.Type} {eventArgs.KeyModifiers}"); + Input?.Invoke(eventArgs); + return eventArgs.Effects; + } public void Dispose() { diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index b94fe2df9e..c4f5e90777 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -24,6 +24,6 @@ namespace Avalonia.Browser public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - public Task GetDataAsync(string format) => Task.FromResult(new()); + public Task GetDataAsync(string format) => Task.FromResult(null); } } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index f1936a8d97..394f191dab 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -1,24 +1,29 @@ -using System.Runtime.InteropServices.JavaScript; +using System; +using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; namespace Avalonia.Browser.Interop; internal static partial class AvaloniaModule { - public const string MainModuleName = "avalonia"; - public const string StorageModuleName = "storage"; - - public static Task ImportMain() + private static readonly Lazy s_importMain = new(() => { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); - } + }); - public static Task ImportStorage() + private static readonly Lazy s_importStorage = new(() => { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js")); - } + }); + + public const string MainModuleName = "avalonia"; + public const string StorageModuleName = "storage"; + + public static Task ImportMain() => s_importMain.Value; + + public static Task ImportStorage() => s_importStorage.Value; [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] public static partial bool IsMobile(); diff --git a/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs new file mode 100644 index 0000000000..6e3b41c05b --- /dev/null +++ b/src/Browser/Avalonia.Browser/Interop/GeneralHelpers.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Browser.Interop; + +internal static partial class GeneralHelpers +{ + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] + public static partial JSObject[] ItemsArrayAt(JSObject jsObject, string key); + public static JSObject[] GetPropertyAsJSObjectArray(this JSObject jsObject, string key) => ItemsArrayAt(jsObject, key); + + [JSImport("GeneralHelpers.itemsArrayAt", AvaloniaModule.MainModuleName)] + public static partial string[] ItemsArrayAtAsStrings(JSObject jsObject, string key); + public static string[] GetPropertyAsStringArray(this JSObject jsObject, string key) => ItemsArrayAtAsStrings(jsObject, key); + + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodString(JSObject jsObject, string name); + [JSImport("GeneralHelpers.callMethod", AvaloniaModule.MainModuleName)] + public static partial string IntCallMethodStringString(JSObject jsObject, string name, string arg1); + + public static string CallMethodString(this JSObject jsObject, string name) => IntCallMethodString(jsObject, name); + public static string CallMethodString(this JSObject jsObject, string name, string arg1) => IntCallMethodStringString(jsObject, name, arg1); +} diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index a816e39da8..a978c18f9b 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -43,13 +43,16 @@ internal static partial class InputHelper [JSMarshalAs>] Func wheel); - [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeInputEvents( JSObject htmlElement, [JSMarshalAs>] Func input); + [JSImport("InputHelper.subscribeDropEvents", AvaloniaModule.MainModuleName)] + public static partial void SubscribeDropEvents(JSObject containerElement, + [JSMarshalAs>] Func dragEvent); + [JSImport("InputHelper.getCoalescedEvents", AvaloniaModule.MainModuleName)] [return: JSMarshalAs>] public static partial JSObject[] GetCoalescedEvents(JSObject pointerEvent); diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 11beba6f2c..2d96ee8d1f 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -46,6 +46,9 @@ internal static partial class StorageHelper [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); + + [JSImport("StorageItems.filesToItemsArray", AvaloniaModule.StorageModuleName)] + public static partial JSObject[] FilesToItemsArray(JSObject item); [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 5b76d53a9d..fc32b3b4f7 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices.JavaScript; -using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia.Browser.Interop; using Avalonia.Platform.Storage; @@ -18,15 +16,13 @@ internal class BrowserStorageProvider : IStorageProvider internal const string PickerCancelMessage = "The user aborted a request"; internal const string NoPermissionsMessage = "Permissions denied"; - private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); - public bool CanOpen => true; public bool CanSave => StorageHelper.HasNativeFilePicker(); public bool CanPickFolder => true; public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, excludeAll) = ConvertFileTypes(options.FileTypeFilter); @@ -60,7 +56,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, excludeAll) = ConvertFileTypes(options.FileTypeChoices); @@ -88,7 +84,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; try @@ -104,14 +100,14 @@ internal class BrowserStorageProvider : IStorageProvider public async Task OpenFileBookmarkAsync(string bookmark) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFile(item) : null; } public async Task OpenFolderBookmarkAsync(string bookmark) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFolder(item) : null; } @@ -128,7 +124,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { - await _lazyModule.Value; + await AvaloniaModule.ImportStorage(); var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch { WellKnownFolder.Desktop => "desktop", diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index 3fb4124c96..80faca7a50 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -5,6 +5,7 @@ import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NavigationHelper } from "./avalonia/navigationHelper"; +import { GeneralHelpers } from "./avalonia/generalHelpers"; export { Caniuse, @@ -15,5 +16,6 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, - NavigationHelper + NavigationHelper, + GeneralHelpers }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts new file mode 100644 index 0000000000..fa001006ab --- /dev/null +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/generalHelpers.ts @@ -0,0 +1,19 @@ +export class GeneralHelpers { + public static itemsArrayAt(instance: any, key: string): any[] { + const items = instance[key]; + if (!items) { + return []; + } + + const retItems = []; + for (let i = 0; i < items.length; i++) { + retItems[i] = items[i]; + } + return retItems; + } + + public static callMethod(instance: any, name: string /*, args */): any { + const args = Array.prototype.slice.call(arguments, 2); + return instance[name].apply(instance, args); + } +} diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 0f0e5eb512..fb94352192 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -174,6 +174,28 @@ export class InputHelper { }; } + public static subscribeDropEvents( + element: HTMLInputElement, + dragEvent: (args: any) => boolean + ) { + const dragHandler = (args: Event) => { + if (dragEvent(args as any)) { + args.preventDefault(); + } + }; + element.addEventListener("dragover", dragHandler); + element.addEventListener("dragenter", dragHandler); + element.addEventListener("dragleave", dragHandler); + element.addEventListener("drop", dragHandler); + + return () => { + element.removeEventListener("dragover", dragHandler); + element.removeEventListener("dragenter", dragHandler); + element.removeEventListener("dragleave", dragHandler); + element.removeEventListener("drop", dragHandler); + }; + } + public static getCoalescedEvents(pointerEvent: PointerEvent): PointerEvent[] { return pointerEvent.getCoalescedEvents(); } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index 8f47e61100..f444717094 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -3,8 +3,9 @@ import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFile import { Caniuse } from "../avalonia"; export class StorageItem { - constructor( + private constructor( public handle?: FileSystemFileHandle | FileSystemDirectoryHandle, + private readonly file?: File, private readonly bookmarkId?: string, public wellKnownType?: WellKnownDirectory ) { @@ -14,6 +15,9 @@ export class StorageItem { if (this.handle) { return this.handle.name; } + if (this.file) { + return this.file.name; + } return this.wellKnownType ?? ""; } @@ -21,14 +25,29 @@ export class StorageItem { if (this.handle) { return this.handle.kind; } + if (this.file) { + return "file"; + } return "directory"; } + public static createFromHandle(handle: FileSystemFileHandle | FileSystemDirectoryHandle, bookmarkId?: string) { + return new StorageItem(handle, undefined, bookmarkId, undefined); + } + + public static createFromFile(file: File) { + return new StorageItem(undefined, file, undefined, undefined); + } + public static createWellKnownDirectory(type: WellKnownDirectory) { - return new StorageItem(undefined, undefined, type); + return new StorageItem(undefined, undefined, undefined, type); } public static async openRead(item: StorageItem): Promise { + if (item.file) { + return item.file; + } + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } @@ -41,7 +60,7 @@ export class StorageItem { public static async openWrite(item: StorageItem): Promise { if (!item.handle || item.kind !== "file") { - throw new Error("StorageItem is not a file"); + throw new Error("StorageItem is not a writeable file"); } await item.verityPermissions("readwrite"); @@ -52,8 +71,9 @@ export class StorageItem { public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { // getFile can fail with an exception depending if we use polyfill with a save file dialog or not. try { - const file = item.handle instanceof FileSystemFileHandle && - await item.handle.getFile(); + const file = item.handle && "getFile" in item.handle + ? await item.handle.getFile() + : item.file; if (!file) { return null; @@ -144,4 +164,16 @@ export class StorageItems { public static itemsArray(instance: StorageItems): StorageItem[] { return instance.items; } + + public static filesToItemsArray(files: File[]): StorageItem[] { + if (!files) { + return []; + } + + const retItems = []; + for (let i = 0; i < files.length; i++) { + retItems[i] = StorageItem.createFromFile(files[i]); + } + return retItems; + } } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index 750c38b8ea..7a29992674 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -19,7 +19,7 @@ export class StorageProvider { }; const handle = await showDirectoryPicker(options as any); - return new StorageItem(handle); + return StorageItem.createFromHandle(handle); } public static async openFileDialog( @@ -33,7 +33,7 @@ export class StorageProvider { }; const handles = await showOpenFilePicker(options); - return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle))); + return new StorageItems(handles.map((handle: FileSystemFileHandle) => StorageItem.createFromHandle(handle))); } public static async saveFileDialog( @@ -48,14 +48,14 @@ export class StorageProvider { // Always prefer native save file picker, as polyfill solutions are not reliable. const handle = await (globalThis as any).showSaveFilePicker(options); - return new StorageItem(handle); + return StorageItem.createFromHandle(handle); } public static async openBookmark(key: string): Promise { const connection = await avaloniaDb.connect(); try { const handle = await connection.get(fileBookmarksStore, key); - return handle && new StorageItem(handle, key); + return handle && StorageItem.createFromHandle(handle, key); } finally { connection.close(); } From b360902d3fd461f048c973525c9abc015ae85432 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 18 Feb 2023 01:33:57 -0500 Subject: [PATCH 26/36] Typo --- src/Avalonia.Base/Input/DataObjectExtensions.cs | 6 +++--- src/Avalonia.Base/Input/IDataObject.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Input/DataObjectExtensions.cs b/src/Avalonia.Base/Input/DataObjectExtensions.cs index 807c242914..6af531b0d8 100644 --- a/src/Avalonia.Base/Input/DataObjectExtensions.cs +++ b/src/Avalonia.Base/Input/DataObjectExtensions.cs @@ -11,7 +11,7 @@ namespace Avalonia.Input /// . /// /// - /// Collection of storage items - files or folders. If format isn't avaialble, returns null. + /// Collection of storage items - files or folders. If format isn't available, returns null. /// public static IEnumerable? GetFiles(this IDataObject dataObject) { @@ -23,7 +23,7 @@ namespace Avalonia.Input /// /// /// - /// Collection of file names. If format isn't avaialble, returns null. + /// Collection of file names. If format isn't available, returns null. /// [System.Obsolete("Use GetFiles, this method is supported only on desktop platforms.")] public static IEnumerable? GetFileNames(this IDataObject dataObject) @@ -40,7 +40,7 @@ namespace Avalonia.Input /// /// /// - /// A text string. If format isn't avaialble, returns null. + /// A text string. If format isn't available, returns null. /// public static string? GetText(this IDataObject dataObject) { diff --git a/src/Avalonia.Base/Input/IDataObject.cs b/src/Avalonia.Base/Input/IDataObject.cs index b6fcd8c7db..6ccd0a8499 100644 --- a/src/Avalonia.Base/Input/IDataObject.cs +++ b/src/Avalonia.Base/Input/IDataObject.cs @@ -25,7 +25,7 @@ namespace Avalonia.Input /// Tries to get the data of the given DataFormat. /// /// - /// Object data. If format isn't avaialble, returns null. + /// Object data. If format isn't available, returns null. /// object? Get(string dataFormat); } From bdebcca9da6d9aebe7146f77df38e5958faf2042 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 19 Feb 2023 12:47:24 +0100 Subject: [PATCH 27/36] Use Tmds.DBus.Protocol package --- .gitmodules | 3 --- Avalonia.Desktop.slnf | 1 - Avalonia.sln | 8 +----- .../Avalonia.FreeDesktop.csproj | 26 +++++++++++++++++-- .../DBusIme/DBusTextInputMethodBase.cs | 1 + .../DBusIme/Fcitx/FcitxICWrapper.cs | 1 + .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 1 + src/Avalonia.FreeDesktop/DBusInterfaces.cs | 19 -------------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 1 + src/Linux/Tmds.DBus | 1 - src/tools/Tmds.DBus.SourceGenerator | 2 +- 11 files changed, 30 insertions(+), 34 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/DBusInterfaces.cs delete mode 160000 src/Linux/Tmds.DBus diff --git a/.gitmodules b/.gitmodules index 2b899e8f5a..c2b5fc3f26 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,6 @@ [submodule "nukebuild/il-repack"] path = nukebuild/il-repack url = https://github.com/Gillibald/il-repack -[submodule "Tmds.DBus"] - path = src/Linux/Tmds.DBus - url = https://github.com/affederaffe/Tmds.DBus [submodule "src/tools/Tmds.DBus.SourceGenerator"] path = src/tools/Tmds.DBus.SourceGenerator url = https://github.com/affederaffe/Tmds.DBus.SourceGenerator diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index c272cb11dd..364529b38d 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -33,7 +33,6 @@ "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", - "src\\Linux\\Tmds.DBus\\src\\Tmds.DBus.Protocol\\Tmds.DBus.Protocol.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 9b20f5e03a..5a13bd43ad 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,9 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.Protocol", "src\Linux\Tmds.DBus\src\Tmds.DBus.Protocol\Tmds.DBus.Protocol.csproj", "{29E25263-3CC3-4D55-A042-00BA136867D4}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.SourceGenerator", "src\tools\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj", "{5E9C0032-E460-4BC1-BCC7-6448F34DD679}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" @@ -551,10 +550,6 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU - {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.Build.0 = Release|Any CPU {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.Build.0 = Debug|Any CPU {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -635,7 +630,6 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {29E25263-3CC3-4D55-A042-00BA136867D4} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {5E9C0032-E460-4BC1-BCC7-6448F34DD679} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index c0f82a6e67..c9ef74617d 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -11,11 +11,17 @@ + + + + + + + + - - @@ -23,4 +29,20 @@ + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 7f99d8190b..d3c14f285d 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -6,6 +6,7 @@ using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme { diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index b651f126fb..00d05e59a3 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index be88b94707..1cf3507cc2 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { diff --git a/src/Avalonia.FreeDesktop/DBusInterfaces.cs b/src/Avalonia.FreeDesktop/DBusInterfaces.cs deleted file mode 100644 index fb2f0ca2f3..0000000000 --- a/src/Avalonia.FreeDesktop/DBusInterfaces.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Tmds.DBus.SourceGenerator; - -namespace Avalonia.FreeDesktop -{ - [DBusInterface("./DBusXml/DBus.xml")] - [DBusInterface("./DBusXml/StatusNotifierWatcher.xml")] - [DBusInterface("./DBusXml/com.canonical.AppMenu.Registrar.xml")] - [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext.xml")] - [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod.xml")] - [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext1.xml")] - [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod1.xml")] - [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] - [DBusInterface("./DBusXml/org.freedesktop.portal.FileChooser.xml")] - [DBusInterface("./DBusXml/org.freedesktop.portal.Request.xml")] - [DBusInterface("./DBusXml/org.freedesktop.portal.Settings.xml")] - [DBusHandler("./DBusXml/DBusMenu.xml")] - [DBusHandler("./DBusXml/StatusNotifierItem.xml")] - internal class DBusInterfaces { } -} diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 521fe68e90..afbee77067 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { diff --git a/src/Linux/Tmds.DBus b/src/Linux/Tmds.DBus deleted file mode 160000 index e86bcf1bc2..0000000000 --- a/src/Linux/Tmds.DBus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e86bcf1bc2d86338ab61663a4ae48dbc0bd7e02d diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator index 309159609e..3e76fbf5be 160000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -1 +1 @@ -Subproject commit 309159609e09f3f0ae77f0abd761360049761700 +Subproject commit 3e76fbf5be03b8e1360e74b8878c541d9de808cd From 1ecdf3252064fafddf6bb82da477cb7cb1e0ea73 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 20 Feb 2023 10:34:21 +0100 Subject: [PATCH 28/36] Remove trailing whitespace for right aligned text Fix justification for non wrapped text Fix text trimming for RTL flow direction --- src/Avalonia.Base/Media/FormattedText.cs | 2 +- .../Media/TextCollapsingCreateInfo.cs | 4 +- .../TextFormatting/InterWordJustification.cs | 13 +- .../Media/TextFormatting/ShapedTextRun.cs | 21 +- .../TextCollapsingProperties.cs | 9 +- .../TextFormatting/TextEllipsisHelper.cs | 191 +++++++++++++----- .../Media/TextFormatting/TextLayout.cs | 25 +-- .../TextLeadingPrefixCharacterEllipsis.cs | 7 +- .../Media/TextFormatting/TextLineImpl.cs | 47 +++-- .../TextTrailingCharacterEllipsis.cs | 7 +- .../TextTrailingWordEllipsis.cs | 7 +- .../Media/TextLeadingPrefixTrimming.cs | 2 +- .../Media/TextTrailingTrimming.cs | 4 +- .../TextFormatting/TextFormatterTests.cs | 2 +- .../Media/TextFormatting/TextLineTests.cs | 2 +- 15 files changed, 231 insertions(+), 112 deletions(-) diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 3b63a98720..d4640390d7 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -877,7 +877,7 @@ namespace Avalonia.Media var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!; - TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps)); + TextCollapsingProperties collapsingProperties = _that._trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(maxLineLength, lastRunProps, paraProps.FlowDirection)); var collapsedLine = line.Collapse(collapsingProperties); diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 40ba613717..9b7bf3f74c 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs @@ -6,11 +6,13 @@ namespace Avalonia.Media { public readonly double Width; public readonly TextRunProperties TextRunProperties; + public readonly FlowDirection FlowDirection; - public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties) + public TextCollapsingCreateInfo(double width, TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; TextRunProperties = textRunProperties; + FlowDirection = flowDirection; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 0d85f3e7c5..c1b9b77401 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -27,16 +27,6 @@ namespace Avalonia.Media.TextFormatting return; } - if (lineImpl.NewLineLength > 0) - { - return; - } - - if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) - { - return; - } - var breakOportunities = new Queue(); var currentPosition = textLine.FirstTextSourceIndex; @@ -97,7 +87,8 @@ namespace Avalonia.Media.TextFormatting continue; } - var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); + var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); + var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 7f23ac98b4..568148e15c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -148,33 +148,38 @@ namespace Avalonia.Media.TextFormatting internal SplitResult Split(int length) { - if (IsReversed) + var isReversed = IsReversed; + + if (isReversed) { Reverse(); - } + length = Length - length; + } #if DEBUG - if(length == 0) + if (length == 0) { throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero."); } -#endif - +#endif var splitBuffer = ShapedBuffer.Split(length); var first = new ShapedTextRun(splitBuffer.First, Properties); - #if DEBUG +#if DEBUG if (first.Length != length) { throw new InvalidOperationException("Split length mismatch."); } - #endif - var second = new ShapedTextRun(splitBuffer.Second!, Properties); + if (isReversed) + { + return new SplitResult(second, first); + } + return new SplitResult(first, second); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 72882df0b5..7cdf81ecc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media.TextFormatting +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -15,6 +17,11 @@ /// public abstract TextRun Symbol { get; } + /// + /// Gets the flow direction that is used for collapsing. + /// + public abstract FlowDirection FlowDirection { get; } + /// /// Collapses given text line. /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 4c93a1d851..6422f23dcd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; namespace Avalonia.Media.TextFormatting @@ -28,97 +27,191 @@ namespace Avalonia.Media.TextFormatting var availableWidth = properties.Width - shapedSymbol.Size.Width; - while (runIndex < textRuns.Count) + if(properties.FlowDirection== FlowDirection.LeftToRight) { - var currentRun = textRuns[runIndex]; - - switch (currentRun) + while (runIndex < textRuns.Count) { - case ShapedTextRun shapedRun: - { - currentWidth += shapedRun.Size.Width; + var currentRun = textRuns[runIndex]; - if (currentWidth > availableWidth) + switch (currentRun) + { + case ShapedTextRun shapedRun: { - if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + currentWidth += shapedRun.Size.Width; + + if (currentWidth > availableWidth) { - if (isWordEllipsis && measuredLength < textLine.Length) + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + if (isWordEllipsis && measuredLength < textLine.Length) { - var nextBreakPosition = lineBreak.PositionMeasure; + var currentBreakPosition = 0; - if (nextBreakPosition == 0) - { - break; - } + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - if (nextBreakPosition >= measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - break; + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } - - measuredLength = currentBreakPosition; } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); } - collapsedLength += measuredLength; + availableWidth -= shapedRun.Size.Width; - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } - availableWidth -= shapedRun.Size.Width; + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.LeftToRight, shapedSymbol); + } - break; - } + availableWidth -= drawableRun.Size.Width; - case DrawableTextRun drawableRun: - { - //The whole run needs to fit into available space - if (currentWidth + drawableRun.Size.Width > availableWidth) - { - return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); + break; } + } - availableWidth -= drawableRun.Size.Width; + collapsedLength += currentRun.Length; - break; - } + runIndex++; } + } + else + { + runIndex = textRuns.Count - 1; + + while (runIndex >= 0) + { + var currentRun = textRuns[runIndex]; - collapsedLength += currentRun.Length; + switch (currentRun) + { + case ShapedTextRun shapedRun: + { + currentWidth += shapedRun.Size.Width; - runIndex++; - } + if (currentWidth > availableWidth) + { + if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength)) + { + if (isWordEllipsis && measuredLength < textLine.Length) + { + var currentBreakPosition = 0; + + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) + { + var nextBreakPosition = lineBreak.PositionMeasure; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition >= measuredLength) + { + break; + } + currentBreakPosition = nextBreakPosition; + } + + measuredLength = currentBreakPosition; + } + } + + collapsedLength += measuredLength; + + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= shapedRun.Size.Width; + + break; + } + + case DrawableTextRun drawableRun: + { + //The whole run needs to fit into available space + if (currentWidth + drawableRun.Size.Width > availableWidth) + { + return CreateCollapsedRuns(textLine, collapsedLength, FlowDirection.RightToLeft, shapedSymbol); + } + + availableWidth -= drawableRun.Size.Width; + + break; + } + } + + collapsedLength += currentRun.Length; + + runIndex--; + } + } + return null; } - private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, - TextRun shapedSymbol) + private static TextRun[] CreateCollapsedRuns(TextLine textLine, int collapsedLength, + FlowDirection flowDirection, TextRun shapedSymbol) { + var textRuns = textLine.TextRuns; + if (collapsedLength <= 0) { return new[] { shapedSymbol }; } + if(flowDirection == FlowDirection.RightToLeft) + { + collapsedLength = textLine.Length - collapsedLength; + } + var objectPool = FormattingObjectPool.Instance; var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); try { - var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; - preSplitRuns.CopyTo(collapsedRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - return collapsedRuns; + if (flowDirection == FlowDirection.RightToLeft) + { + var collapsedRuns = new TextRun[postSplitRuns!.Count + 1]; + postSplitRuns.CopyTo(collapsedRuns, 1); + collapsedRuns[0] = shapedSymbol; + return collapsedRuns; + } + else + { + var collapsedRuns = new TextRun[preSplitRuns!.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } finally { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4dbc472133..a382416b8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting var lastTrailingIndex = 0; - if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; @@ -377,7 +377,7 @@ namespace Avalonia.Media.TextFormatting { lastTrailingIndex += textEndOfLine.Length; } - } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -553,26 +553,18 @@ namespace Avalonia.Media.TextFormatting if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { - var whitespaceWidth = 0d; + var justificationWidth = MaxWidth; - for (var i = 0; i < textLines.Count; i++) + if (_paragraphProperties.TextWrapping != TextWrapping.NoWrap) { - var line = textLines[i]; - var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - - if (lineWhitespaceWidth > whitespaceWidth) - { - whitespaceWidth = lineWhitespaceWidth; - } + justificationWidth = width; } - var justificationWidth = width - whitespaceWidth; - if (justificationWidth > 0) { var justificationProperties = new InterWordJustification(justificationWidth); - for (var i = 0; i < textLines.Count - 1; i++) + for (var i = 0; i < textLines.Count; i++) { var line = textLines[i]; @@ -597,12 +589,13 @@ namespace Avalonia.Media.TextFormatting /// The . private TextCollapsingProperties? GetCollapsingProperties(double width) { - if(_textTrimming == TextTrimming.None) + if (_textTrimming == TextTrimming.None) { return null; } - return _textTrimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties)); + return _textTrimming.CreateCollapsingProperties( + new TextCollapsingCreateInfo(width, _paragraphProperties.DefaultTextRunProperties, _paragraphProperties.FlowDirection)); } public void Dispose() diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 2e85b1e187..a21a5d45e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -19,11 +19,13 @@ namespace Avalonia.Media.TextFormatting /// Length of leading prefix. /// width in which collapsing is constrained to /// text run properties of ellipsis symbol + /// the flow direction of the collapes line. public TextLeadingPrefixCharacterEllipsis( string ellipsis, int prefixLength, double width, - TextRunProperties textRunProperties) + TextRunProperties textRunProperties, + FlowDirection flowDirection) { if (_prefixLength < 0) { @@ -33,6 +35,7 @@ namespace Avalonia.Media.TextFormatting _prefixLength = prefixLength; Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -41,6 +44,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 187b3154ad..b3321d4d9f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting currentX += drawableTextRun.Size.Width; } - if(lastRunIndex - 1 < 0) + if (lastRunIndex - 1 < 0) { break; } @@ -685,7 +685,7 @@ namespace Avalonia.Media.TextFormatting directionalWidth -= drawableTextRun.Size.Width; } - if(firstRunIndex + 1 == _textRuns.Length) + if (firstRunIndex + 1 == _textRuns.Length) { break; } @@ -1097,7 +1097,7 @@ namespace Avalonia.Media.TextFormatting var runWidth = endX - startX; - return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override void Dispose() @@ -1439,13 +1439,6 @@ namespace Avalonia.Media.TextFormatting } } - if (index == lastRunIndex) - { - width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; - trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength += textRun.GlyphRun.Metrics.NewLineLength; - } - widthIncludingWhitespace += textRun.Size.Width; break; @@ -1455,12 +1448,6 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - } - if (drawableTextRun.Size.Height > height) { height = drawableTextRun.Size.Height; @@ -1476,6 +1463,32 @@ namespace Avalonia.Media.TextFormatting } } + width = widthIncludingWhitespace; + + for (var i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + + if(currentRun is ShapedTextRun shapedText) + { + var glyphRun = shapedText.GlyphRun; + var glyphRunMetrics = glyphRun.Metrics; + + newLineLength += glyphRunMetrics.NewLineLength; + + if (glyphRunMetrics.TrailingWhitespaceLength == 0) + { + break; + } + + trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; + + var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + + width -= whitespaceWidth; + } + } + var start = GetParagraphOffsetX(width, widthIncludingWhitespace); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) @@ -1543,7 +1556,7 @@ namespace Avalonia.Media.TextFormatting return Math.Max(0, start); case TextAlignment.Right: - return Math.Max(0, _paragraphWidth - widthIncludingTrailingWhitespace); + return Math.Max(0, _paragraphWidth - width); default: return 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index ccae99cc75..8a6607bce2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -12,10 +12,13 @@ /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) + /// The flow direction of the collapsed line. + public TextTrailingCharacterEllipsis(string ellipsis, double width, + TextRunProperties textRunProperties, FlowDirection flowDirection) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -24,6 +27,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c622c76a60..5252766382 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -12,14 +12,17 @@ /// Text used as collapsing symbol. /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. + /// flow direction of the collapsed line. public TextTrailingWordEllipsis( string ellipsis, double width, - TextRunProperties textRunProperties + TextRunProperties textRunProperties, + FlowDirection flowDirection ) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); + FlowDirection = flowDirection; } /// @@ -28,6 +31,8 @@ /// public override TextRun Symbol { get; } + public override FlowDirection FlowDirection { get; } + /// public override TextRun[]? Collapse(TextLine textLine) { diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 7ba25eb005..19e6a70357 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) { - return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties); + return new TextLeadingPrefixCharacterEllipsis(_ellipsis, _prefixLength, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 2edbaabbc6..8a3c5aa397 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrailingTrimming.cs @@ -17,10 +17,10 @@ namespace Avalonia.Media { if (_isWordBased) { - return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingWordEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } - return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties); + return new TextTrailingCharacterEllipsis(_ellipsis, createInfo.Width, createInfo.TextRunProperties, createInfo.FlowDirection); } public override string ToString() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 8a2d4ecc6b..dc8744b292 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -457,7 +457,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties, FlowDirection.LeftToRight)); } currentHeight += textLine.Height; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 70e74cdf83..bd64d72a4d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -407,7 +407,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.False(textLine.HasCollapsed); - TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties)); + TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties, FlowDirection.LeftToRight)); var collapsedLine = textLine.Collapse(collapsingProperties); From 79d0e9c5b416320fef0c0185888df9c1e8c08ff6 Mon Sep 17 00:00:00 2001 From: Stan Wijckmans Date: Mon, 20 Feb 2023 13:56:46 +0100 Subject: [PATCH 29/36] Make RequestPlatformInhibition public. --- src/Avalonia.Controls/TopLevel.cs | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index ed11dec1d0..eea175eb3c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -405,7 +405,31 @@ namespace Avalonia.Controls { return visual?.VisualRoot as TopLevel; } - + + /// + /// Requests a to be inhibited. + /// The behavior remains inhibited until the return value is disposed. + /// The available set of s depends on the platform. + /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. + /// + public async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) + { + var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); + if (platformBehaviorInhibition == null) + { + return Disposable.Create(() => { }); + } + + switch (type) + { + case PlatformInhibitionType.AppSleep: + await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); + return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); + default: + return Disposable.Create(() => { }); + } + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -571,30 +595,6 @@ namespace Avalonia.Controls /// The event args. protected virtual void OnClosed(EventArgs e) => Closed?.Invoke(this, e); - /// - /// Requests a to be inhibited. - /// The behavior remains inhibited until the return value is disposed. - /// The available set of s depends on the platform. - /// If a behavior is inhibited on a platform where this type is not supported the request will have no effect. - /// - protected async Task RequestPlatformInhibition(PlatformInhibitionType type, string reason) - { - var platformBehaviorInhibition = PlatformImpl?.TryGetFeature(); - if (platformBehaviorInhibition == null) - { - return Disposable.Create(() => { }); - } - - switch (type) - { - case PlatformInhibitionType.AppSleep: - await platformBehaviorInhibition.SetInhibitAppSleep(true, reason); - return Disposable.Create(() => platformBehaviorInhibition.SetInhibitAppSleep(false, reason).Wait()); - default: - return Disposable.Create(() => { }); - } - } - /// /// Tries to get a service from an , logging a /// warning if not found. From f5caa6105188203e5ae789cd12efd7d20caa7c61 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 20 Feb 2023 18:10:23 +0100 Subject: [PATCH 30/36] [SKIA] UseOpacitySaveLayer feature switch (#9964) * [SKIA] Introduce UseOpacitySaveLayer feature switch * Only maintain _currentOpacity when OpacitySaveLayer is disabled --- src/Avalonia.Base/Media/DrawingContext.cs | 5 ++- src/Avalonia.Base/Media/DrawingGroup.cs | 8 ++-- .../Media/ImmediateDrawingContext.cs | 5 ++- .../Platform/IDrawingContextImpl.cs | 2 +- .../Drawing/CompositionDrawingContext.cs | 6 +-- .../Composition/Server/DrawingContextProxy.cs | 4 +- .../Server/ServerCompositionVisual.cs | 13 +++--- .../Rendering/ImmediateRenderer.cs | 2 +- .../Rendering/SceneGraph/OpacityNode.cs | 11 +++-- .../HeadlessPlatformRenderInterface.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 45 ++++++++++++++----- src/Skia/Avalonia.Skia/SkiaOptions.cs | 8 ++++ .../Media/DrawingContextImpl.cs | 5 ++- .../NullDrawingContextImpl.cs | 2 +- .../Controls/CustomRenderTests.cs | 2 +- .../Avalonia.RenderTests/Media/BitmapTests.cs | 2 +- 16 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 622181dba0..a37fa6fd32 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -361,11 +361,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index b7abda2c61..7b02649b6c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -74,10 +74,12 @@ namespace Avalonia.Media public override void Draw(DrawingContext context) { + var bounds = GetBounds(); + using (context.PushPreTransform(Transform?.Value ?? Matrix.Identity)) - using (context.PushOpacity(Opacity)) + using (context.PushOpacity(Opacity, bounds)) using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default) - using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, GetBounds()) : default) + using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default) { foreach (var drawing in Children) { @@ -284,7 +286,7 @@ namespace Avalonia.Media throw new NotImplementedException(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 7d9534c414..2564d89bac 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -281,11 +281,12 @@ namespace Avalonia.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public PushedState PushOpacity(double opacity) + public PushedState PushOpacity(double opacity, Rect bounds) //TODO: Eliminate platform-specific push opacity call { - PlatformImpl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity, bounds); return new PushedState(this, PushedState.PushedStateType.Opacity); } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 8509067cd0..8962bc1586 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Platform /// Pushes an opacity value. /// /// The opacity. - void PushOpacity(double opacity); + void PushOpacity(double opacity, Rect bounds); /// /// Pops the latest pushed opacity value. diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index b75d080cfd..6b380608fe 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -313,13 +313,13 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(opacity)) + if (next == null || !next.Item.Equals(opacity, bounds)) { - Add(new OpacityNode(opacity)); + Add(new OpacityNode(opacity, bounds)); } else { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 50df8bd32b..08e506536f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -111,9 +111,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.PopClip(); } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { - _impl.PushOpacity(opacity); + _impl.PushOpacity(opacity, bounds); } public void PopOpacity() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 98be861afa..f9492d0015 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -41,9 +41,9 @@ namespace Avalonia.Rendering.Composition.Server return; Root!.RenderedVisuals++; - - if (Opacity != 1) - canvas.PushOpacity(Opacity); + + var boundsRect = new Rect(new Size(Size.X, Size.Y)); + if (AdornedVisual != null) { canvas.PostTransform = Matrix.Identity; @@ -54,15 +54,16 @@ namespace Avalonia.Rendering.Composition.Server var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; - - var boundsRect = new Rect(new Size(Size.X, Size.Y)); + + if (Opacity != 1) + canvas.PushOpacity(Opacity, boundsRect); if (ClipToBounds && !HandlesClipToBounds) canvas.PushClip(Root!.SnapToDevicePixels(boundsRect)); if (Clip != null) canvas.PushGeometryClip(Clip); if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 8e5dc38317..09d2d55ce3 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -117,7 +117,7 @@ namespace Avalonia.Rendering } using (context.PushPostTransform(m)) - using (context.PushOpacity(opacity)) + using (context.PushOpacity(opacity, bounds)) using (clipToBounds #pragma warning disable CS0618 // Type or member is obsolete ? visual is IVisualWithRoundRectClip roundClipVisual diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index e41e639067..f76a055934 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -12,9 +12,11 @@ namespace Avalonia.Rendering.SceneGraph /// opacity push. /// /// The opacity to push. - public OpacityNode(double opacity) + /// The bounds. + public OpacityNode(double opacity, Rect bounds) { Opacity = opacity; + Bounds = bounds; } /// @@ -26,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => default; + public Rect Bounds { get; } /// /// Gets the opacity to be pushed or null if the operation represents a pop. @@ -40,19 +42,20 @@ namespace Avalonia.Rendering.SceneGraph /// Determines if this draw operation equals another. /// /// The opacity of the other draw operation. + /// The bounds of the other draw operation. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(double? opacity) => Opacity == opacity; + public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; /// public void Render(IDrawingContextImpl context) { if (Opacity.HasValue) { - context.PushOpacity(Opacity.Value); + context.PushOpacity(Opacity.Value, Bounds); } else { diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 68466fe381..5b84ceef7f 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -392,7 +392,7 @@ namespace Avalonia.Headless } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect rect) { } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 82d902cbd0..969f0b5e2a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -40,6 +40,7 @@ namespace Avalonia.Skia private static SKShader? s_acrylicNoiseShader; private readonly ISkiaGpuRenderSession? _session; private bool _leased; + private bool _useOpacitySaveLayer; /// /// Context create info. @@ -158,6 +159,13 @@ namespace Avalonia.Skia } Transform = Matrix.Identity; + + var options = AvaloniaLocator.Current.GetService(); + + if(options != null) + { + _useOpacitySaveLayer = options.UseOpacitySaveLayer; + } } /// @@ -188,7 +196,7 @@ namespace Avalonia.Skia var d = destRect.ToSKRect(); var paint = SKPaintCache.Shared.Get(); - paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); + paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity))); paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); @@ -373,7 +381,7 @@ namespace Avalonia.Skia { if (!boxShadow.IsDefault && !boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) { var spread = (float)boxShadow.Spread; if (boxShadow.IsInset) @@ -430,7 +438,7 @@ namespace Avalonia.Skia { if (!boxShadow.IsDefault && boxShadow.IsInset) { - using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) + using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) { var spread = (float)boxShadow.Spread; var offsetX = (float)boxShadow.OffsetX; @@ -568,18 +576,35 @@ namespace Avalonia.Skia } /// - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { CheckLease(); - _opacityStack.Push(_currentOpacity); - _currentOpacity *= opacity; + + if(_useOpacitySaveLayer) + { + var rect = bounds.ToSKRect(); + Canvas.SaveLayer(rect, new SKPaint { ColorF = new SKColorF(0, 0, 0, (float)opacity)}); + } + else + { + _opacityStack.Push(_currentOpacity); + _currentOpacity *= opacity; + } } /// public void PopOpacity() { CheckLease(); - _currentOpacity = _opacityStack.Pop(); + + if(_useOpacitySaveLayer) + { + Canvas.Restore(); + } + else + { + _currentOpacity = _opacityStack.Pop(); + } } /// @@ -657,7 +682,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); - Canvas.SaveLayer(paint); + Canvas.SaveLayer(bounds.ToSKRect(), paint); _maskStack.Push(CreatePaint(paint, mask, bounds.Size)); } @@ -1021,8 +1046,6 @@ namespace Avalonia.Skia paint.IsAntialias = true; - double opacity = _currentOpacity; - var tintOpacity = material.BackgroundSource == AcrylicBackgroundSource.Digger ? material.TintOpacity : 1; @@ -1071,7 +1094,7 @@ namespace Avalonia.Skia paint.IsAntialias = true; - double opacity = brush.Opacity * _currentOpacity; + double opacity = brush.Opacity * (_useOpacitySaveLayer ? 1 :_currentOpacity); if (brush is ISolidColorBrush solid) { diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index b3c3056a58..84ad547d6c 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -16,5 +16,13 @@ namespace Avalonia /// Setting this to null will give you the default Skia value. /// public long? MaxGpuResourceSizeBytes { get; set; } = 1024 * 600 * 4 * 12; // ~28mb 12x 1024 x 600 textures. + + /// + /// Use Skia's SaveLayer API to handling opacity. + /// + /// + /// Enabling this might have performance implications. + /// + public bool UseOpacitySaveLayer { get; set; } = false; } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 3506abc63b..0dd9c155bb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -441,14 +441,15 @@ namespace Avalonia.Direct2D1.Media /// Pushes an opacity value. /// /// The opacity. + /// The bounds. /// A disposable used to undo the opacity. - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { if (opacity < 1) { var parameters = new LayerParameters { - ContentBounds = PrimitiveExtensions.RectangleInfinite, + ContentBounds = bounds.ToDirect2D(), MaskTransform = PrimitiveExtensions.Matrix3x2Identity, Opacity = (float)opacity, }; diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index e83b2d7598..40d504a0ac 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -65,7 +65,7 @@ namespace Avalonia.Benchmarks { } - public void PushOpacity(double opacity) + public void PushOpacity(double opacity, Rect bounds) { } diff --git a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs index 14f5b7c6c7..1199184d14 100644 --- a/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs +++ b/tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs @@ -141,7 +141,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls new Rect(control.Bounds.Size), 4); - using (context.PushOpacity(0.5)) + using (context.PushOpacity(0.5, control.Bounds)) { context.FillRectangle( Brushes.Blue, diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index c63a876d81..05e160dca8 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -79,7 +79,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media using (var ctx = target.CreateDrawingContext(null)) { ctx.Clear(Colors.Transparent); - ctx.PushOpacity(0.8); + ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); ctx.DrawRectangle(Brushes.Chartreuse, null, new Rect(0, 0, 20, 100)); ctx.DrawRectangle(Brushes.Crimson, null, new Rect(20, 0, 20, 100)); ctx.DrawRectangle(Brushes.Gold,null, new Rect(40, 0, 20, 100)); From c8cbbc2c259359c1990423735022121c637543fe Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 20 Feb 2023 18:30:13 +0100 Subject: [PATCH 31/36] Use SG NuGet package --- NuGet.Config | 1 - src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index 7a9dd7993c..7d2bd8abd2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,6 +6,5 @@ - diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index c9ef74617d..51af5862cf 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -13,16 +13,12 @@ - - - - + - From 8bcb5250d2d08566a143ba6f379456a72fba5a0d Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 20 Feb 2023 18:31:12 +0100 Subject: [PATCH 32/36] Remove git submodule --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index c2b5fc3f26..032bc879cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,3 @@ [submodule "nukebuild/il-repack"] path = nukebuild/il-repack url = https://github.com/Gillibald/il-repack -[submodule "src/tools/Tmds.DBus.SourceGenerator"] - path = src/tools/Tmds.DBus.SourceGenerator - url = https://github.com/affederaffe/Tmds.DBus.SourceGenerator From f68d17dcf69ebc68ce6343851148dbadb9274372 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 20 Feb 2023 18:38:27 +0100 Subject: [PATCH 33/36] Remove git submodule the second --- src/tools/Tmds.DBus.SourceGenerator | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/tools/Tmds.DBus.SourceGenerator diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator deleted file mode 160000 index 3e76fbf5be..0000000000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3e76fbf5be03b8e1360e74b8878c541d9de808cd From e77bc1960a1304558eaebe14146cd514820d798d Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 21 Feb 2023 00:53:28 +0100 Subject: [PATCH 34/36] Remove submodule leftovers --- Avalonia.sln | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index d6a1fb236c..56847bae31 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,9 +231,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.SourceGenerator", "src\tools\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj", "{5E9C0032-E460-4BC1-BCC7-6448F34DD679}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}" @@ -558,11 +555,6 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU - {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.Build.0 = Release|Any CPU - {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU @@ -642,7 +634,6 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {5E9C0032-E460-4BC1-BCC7-6448F34DD679} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} From cbcff7bf1d9d8bf8c3311029077b84c11fe2536a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 21 Feb 2023 03:19:25 -0500 Subject: [PATCH 35/36] Use popup theme instead of target, as it is correct --- src/Avalonia.Controls/Primitives/Popup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 142db71761..9d443d9289 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -392,7 +392,7 @@ namespace Avalonia.Controls.Primitives topLevelPopup .Bind( ThemeVariantScope.ActualThemeVariantProperty, - placementTarget.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) + this.GetBindingObservable(ThemeVariantScope.ActualThemeVariantProperty)) .DisposeWith(handlerCleanup); } From 74ef92a4a8f5575472d02c8ffed01774d118cd2e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 21 Feb 2023 03:19:41 -0500 Subject: [PATCH 36/36] Make StyledElement use theme variants again --- src/Avalonia.Base/StyledElement.cs | 21 ++++++++++++++++++++- src/Avalonia.Base/Styling/ThemeVariant.cs | 15 +++++++++++++++ src/Avalonia.Controls/Control.cs | 18 +----------------- src/Avalonia.Controls/ThemeVariantScope.cs | 16 ++++------------ 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 405b874fed..94af8385a8 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -28,7 +28,7 @@ namespace Avalonia public class StyledElement : Animatable, IDataContextProvider, ILogical, - IResourceHost, + IThemeVariantHost, IStyleHost, IStyleable, ISetLogicalParent, @@ -143,6 +143,9 @@ namespace Avalonia /// public event EventHandler? ResourcesChanged; + /// + public event EventHandler? ActualThemeVariantChanged; + /// /// Gets or sets the name of the styled element. /// @@ -299,6 +302,9 @@ namespace Avalonia /// public StyledElement? Parent { get; private set; } + /// + public ThemeVariant ActualThemeVariant => GetValue(ThemeVariant.ActualThemeVariantProperty); + /// /// Gets the styled element's logical parent. /// @@ -618,7 +624,20 @@ namespace Avalonia base.OnPropertyChanged(change); if (change.Property == ThemeProperty) + { OnControlThemeChanged(); + } + else if (change.Property == ThemeVariant.RequestedThemeVariantProperty) + { + if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) + SetValue(ThemeVariant.ActualThemeVariantProperty, themeVariant); + else + ClearValue(ThemeVariant.ActualThemeVariantProperty); + } + else if (change.Property == ThemeVariant.ActualThemeVariantProperty) + { + ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); + } } private protected virtual void OnControlThemeChanged() diff --git a/src/Avalonia.Base/Styling/ThemeVariant.cs b/src/Avalonia.Base/Styling/ThemeVariant.cs index 5a6a8785a5..389136b0f5 100644 --- a/src/Avalonia.Base/Styling/ThemeVariant.cs +++ b/src/Avalonia.Base/Styling/ThemeVariant.cs @@ -11,6 +11,21 @@ namespace Avalonia.Styling; [TypeConverter(typeof(ThemeVariantTypeConverter))] public sealed record ThemeVariant { + /// + /// Defines the ActualThemeVariant property. + /// + internal static readonly StyledProperty ActualThemeVariantProperty = + AvaloniaProperty.Register( + "ActualThemeVariant", + inherits: true); + + /// + /// Defines the RequestedThemeVariant property. + /// + internal static readonly StyledProperty RequestedThemeVariantProperty = + AvaloniaProperty.Register( + "RequestedThemeVariant", defaultValue: Default); + /// /// Creates a new instance of the /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index c9e8f0d045..dbece12575 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls /// - A property to allow user-defined data to be attached to the control. /// - and other context menu related members. /// - public class Control : InputElement, IDataTemplateHost, INamed, IVisualBrushInitialize, ISetterValue, IThemeVariantHost + public class Control : InputElement, IDataTemplateHost, INamed, IVisualBrushInitialize, ISetterValue { /// /// Defines the property. @@ -162,11 +162,6 @@ namespace Avalonia.Controls set => SetValue(TagProperty, value); } - /// - public ThemeVariant ActualThemeVariant => GetValue(ThemeVariantScope.ActualThemeVariantProperty); - - public event EventHandler? ActualThemeVariantChanged; - /// /// Occurs when the user has completed a context input gesture, such as a right-click. /// @@ -531,17 +526,6 @@ namespace Avalonia.Controls RaiseEvent(sizeChangedEventArgs); } } - else if (change.Property == ThemeVariantScope.RequestedThemeVariantProperty) - { - if (change.GetNewValue() is {} themeVariant && themeVariant != ThemeVariant.Default) - SetValue(ThemeVariantScope.ActualThemeVariantProperty, themeVariant); - else - ClearValue(ThemeVariantScope.ActualThemeVariantProperty); - } - else if (change.Property == ThemeVariantScope.ActualThemeVariantProperty) - { - ActualThemeVariantChanged?.Invoke(this, EventArgs.Empty); - } } } } diff --git a/src/Avalonia.Controls/ThemeVariantScope.cs b/src/Avalonia.Controls/ThemeVariantScope.cs index b29f1ee56e..f5ad4b2f94 100644 --- a/src/Avalonia.Controls/ThemeVariantScope.cs +++ b/src/Avalonia.Controls/ThemeVariantScope.cs @@ -7,21 +7,13 @@ namespace Avalonia.Controls /// public class ThemeVariantScope : Decorator { - /// - /// Defines the property. - /// + /// public static readonly StyledProperty ActualThemeVariantProperty = - AvaloniaProperty.Register( - nameof(ActualThemeVariant), - inherits: true); + ThemeVariant.ActualThemeVariantProperty.AddOwner(); - /// - /// Defines the property. - /// + /// public static readonly StyledProperty RequestedThemeVariantProperty = - AvaloniaProperty.Register( - nameof(RequestedThemeVariant), - defaultValue: ThemeVariant.Default); + ThemeVariant.RequestedThemeVariantProperty.AddOwner(); /// /// Gets or sets the UI theme variant that is used by the control (and its child elements) for resource determination.