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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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/60] 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 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 14/60] 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 b285b88b8e216538440c3ec48829ea9cbfc4c128 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:15:22 +0100 Subject: [PATCH 15/60] Added some failing virtualization tests. --- .../VirtualizingStackPanelTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index f1dd874c71..55c43f6f96 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -278,6 +279,82 @@ namespace Avalonia.Controls.UnitTests Assert.Same(focused, target.GetRealizedElements().First()); } + [Fact] + public void Removing_Range_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 80); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new AvaloniaList(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.RemoveRange(0, 95); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 10, 10); + Assert.Equal(new Vector(0, 100), scroll.Offset); + } + + [Fact] + public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport() + { + using var app = App(); + var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}")); + var (target, scroll, itemsControl) = CreateTarget(items: items); + + scroll.Offset = new Vector(0, 900); + Layout(target); + + AssertRealizedItems(target, itemsControl, 90, 10); + + items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}")); + Layout(target); + + AssertRealizedItems(target, itemsControl, 0, 5); + Assert.Equal(new Vector(0, 0), scroll.Offset); + } + private static IReadOnlyList GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl) { return target.GetRealizedElements() @@ -378,5 +455,24 @@ namespace Avalonia.Controls.UnitTests } private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus); + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(IEnumerable items) + { + AddRange(items); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + } } } From 10fae098b9d531ff08be542a05861dca919ed3a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:16:30 +0100 Subject: [PATCH 16/60] Fix index clamping. - Before we were clamping indexes too early, meaning that `firstIndexU` was calculated with a non-clamped index - `_startUUnstable` needs to be set when the remove happens before the realized elements --- src/Avalonia.Controls/VirtualizingStackPanel.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 634efbd699..8e7690aa6c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls if (firstIndex == -1) { estimatedElementSize = EstimateElementSizeU(); - firstIndex = (int)(viewportStart / estimatedElementSize); + firstIndex = Math.Min((int)(viewportStart / estimatedElementSize), maxIndex); firstIndexU = firstIndex * estimatedElementSize; } @@ -411,13 +411,13 @@ namespace Avalonia.Controls { if (estimatedElementSize == -1) estimatedElementSize = EstimateElementSizeU(); - lastIndex = (int)(viewportEnd / estimatedElementSize); + lastIndex = Math.Min((int)(viewportEnd / estimatedElementSize), maxIndex); } return new MeasureViewport { - firstIndex = MathUtilities.Clamp(firstIndex, 0, maxIndex), - lastIndex = MathUtilities.Clamp(lastIndex, 0, maxIndex), + firstIndex = firstIndex, + lastIndex = lastIndex, viewportUStart = viewportStart, viewportUEnd = viewportEnd, startU = firstIndexU, @@ -1131,6 +1131,7 @@ namespace Avalonia.Controls // The removed range was before the realized elements. Update the first index and // the indexes of the realized elements. _firstIndex -= count; + _startUUnstable = true; var newIndex = _firstIndex; for (var i = 0; i < _elements.Count; ++i) From 68074ce4b150f16582fe21c8688781bdd48b49c4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:16:41 +0100 Subject: [PATCH 17/60] Added failing ScrollViewer test. --- .../ScrollViewerTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index c3d35653cc..d3eb42f147 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -237,6 +237,40 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Reducing_Extent_Should_Constrain_Offset() + { + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + }; + var root = new TestRoot(target); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(50, 50); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(new Vector(-30, -30), e.ExtentDelta); + Assert.Equal(new Vector(-30, -30), e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(70, 70)); + + Assert.Equal(0, raised); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(1, raised); + Assert.Equal(new Vector(20, 20), target.Offset); + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid From 7da9bb9d434ecb70e5ef2fc7af768067c43709f1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 15:17:40 +0100 Subject: [PATCH 18/60] Coerce offset in SCP arrange. If viewport or extent were changed, this could affect the current offset so make sure we coerce the offset during arrange. --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 454f7eac9d..bc86558ab3 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -427,6 +427,7 @@ namespace Avalonia.Controls.Presenters Viewport = finalSize; Extent = Child!.Bounds.Size.Inflate(Child.Margin); + Offset = ScrollViewer.CoerceOffset(Extent, finalSize, Offset); _isAnchorElementDirty = true; return finalSize; From 02906a623485971475cd2095987c8f8d3caa9cdf Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Mon, 13 Feb 2023 18:04:22 +0200 Subject: [PATCH 19/60] Add SliderAutomationPeer --- .../Automation/Peers/SliderAutomationPeer.cs | 22 +++++++ src/Avalonia.Controls/Slider.cs | 6 ++ .../Automation/AutomationNode.cs | 65 ++++++------------- .../Automation/RootAutomationNode.cs | 35 +++++++++- 4 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 0000000000..42b15eec96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(Slider owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Slider"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + + } +} diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 828bf2a1fb..7de726a932 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Automation; +using Avalonia.Controls.Automation.Peers; namespace Avalonia.Controls { @@ -380,6 +381,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new SliderAutomationPeer(this); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 29ab2cea3a..d35443b339 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -22,7 +22,6 @@ namespace Avalonia.Win32.Automation IRawElementProviderSimple, IRawElementProviderSimple2, IRawElementProviderFragment, - IRawElementProviderAdviseEvents, IInvokeProvider { private static Dictionary s_propertyMap = new Dictionary() @@ -50,14 +49,31 @@ namespace Avalonia.Win32.Automation new ConditionalWeakTable(); private readonly int[] _runtimeId; - private int _raiseFocusChanged; - private int _raisePropertyChanged; public AutomationNode(AutomationPeer peer) { _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; s_nodes.Add(peer, this); + peer.ChildrenChanged += Peer_ChildrenChanged; + peer.PropertyChanged += Peer_PropertyChanged; + } + + private void Peer_ChildrenChanged(object sender, EventArgs e) + { + ChildrenChanged(); + } + + private void Peer_PropertyChanged(object sender, AutomationPropertyChangedEventArgs e) + { + if (s_propertyMap.TryGetValue(e.Property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( + this, + (int)id, + e.OldValue as IConvertible, + e.NewValue as IConvertible); + } } public AutomationPeer Peer { get; protected set; } @@ -89,14 +105,6 @@ namespace Avalonia.Win32.Automation 0); } - public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) - { - if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id)) - { - UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue); - } - } - [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { @@ -190,32 +198,6 @@ namespace Avalonia.Win32.Automation void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); - void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationPropertyChanged: - ++_raisePropertyChanged; - break; - case UiaEventId.AutomationFocusChanged: - ++_raiseFocusChanged; - break; - } - } - - void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationPropertyChanged: - --_raisePropertyChanged; - break; - case UiaEventId.AutomationFocusChanged: - --_raiseFocusChanged; - break; - } - } - protected void InvokeSync(Action action) { if (Dispatcher.UIThread.CheckAccess()) @@ -268,15 +250,6 @@ namespace Avalonia.Win32.Automation throw new NotSupportedException(); } - protected void RaiseFocusChanged(AutomationNode? focused) - { - if (_raiseFocusChanged > 0) - { - UiaCoreProviderApi.UiaRaiseAutomationEvent( - focused, - (int)UiaEventId.AutomationFocusChanged); - } - } private AutomationNode? GetRoot() { diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index b732c4169f..1c6784798e 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -12,8 +12,11 @@ namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot + IRawElementProviderFragmentRoot, + IRawElementProviderAdviseEvents { + private int _raiseFocusChanged; + public RootAutomationNode(AutomationPeer peer) : base(peer) { @@ -44,6 +47,36 @@ namespace Avalonia.Win32.Automation return GetOrCreate(focus); } + void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationFocusChanged: + ++_raiseFocusChanged; + break; + } + } + + void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationFocusChanged: + --_raiseFocusChanged; + break; + } + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + if (_raiseFocusChanged > 0) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + } + public void FocusChanged(object? sender, EventArgs e) { RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); From 37f5f50212cce6d4ee61d10f20cc6fcc309d1c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 13 Feb 2023 22:41:41 +0100 Subject: [PATCH 20/60] Update Layoutable.cs --- src/Avalonia.Base/Layout/Layoutable.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..f14ad3058a 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -323,12 +323,15 @@ namespace Avalonia.Layout set { SetValue(UseLayoutRoundingProperty, value); } } - internal Size? PreviousMeasure => _previousMeasure; + /// + /// Gets the available size passed in the previous layout pass, if any. + /// + public Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// - internal Rect? PreviousArrange => _previousArrange; + public Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary From ac21ab2fe69539522296a6d043ebbb98dcaeaaaf Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 14 Feb 2023 15:06:13 +0200 Subject: [PATCH 21/60] Add Slider Automation Test --- samples/IntegrationTestApp/MainWindow.axaml | 3 ++ .../SliderTests.cs | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/Avalonia.IntegrationTests.Appium/SliderTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..8fd33061df 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -151,6 +151,9 @@ + + + diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs new file mode 100644 index 0000000000..f6c50af59d --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -0,0 +1,35 @@ +using System; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class SliderTests + { + private readonly AppiumDriver _session; + + public SliderTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Slider"); + tab.Click(); + } + + [Fact] + public void Changes_Value_When_Moving_Slider() + { + var slider = _session.FindElementByAccessibilityId("Slider2"); + + // slider.Text gets the Slider value + Assert.True(double.Parse(slider.Text) == 30); + + new Actions(_session).Click(slider).MoveByOffset(100, 0).Perform(); + + Assert.Equal(50, Math.Round(double.Parse(slider.Text))); + } + } +} From 4ad731ea60bc11bba5dc35e9d3ebd65ce4bd04bd Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 14 Feb 2023 15:38:03 +0200 Subject: [PATCH 22/60] Fix nullability issues --- src/Windows/Avalonia.Win32/Automation/AutomationNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index d35443b339..0642331b74 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -59,12 +59,12 @@ namespace Avalonia.Win32.Automation peer.PropertyChanged += Peer_PropertyChanged; } - private void Peer_ChildrenChanged(object sender, EventArgs e) + private void Peer_ChildrenChanged(object? sender, EventArgs e) { ChildrenChanged(); } - private void Peer_PropertyChanged(object sender, AutomationPropertyChangedEventArgs e) + private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) { if (s_propertyMap.TryGetValue(e.Property, out var id)) { From 1fbd4ab801c53dc913ff6bdc8f3525d6c578d174 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sun, 29 Jan 2023 22:07:35 +0100 Subject: [PATCH 23/60] Added Avalonia.Analyzers and ten diagonstics --- Avalonia.Desktop.slnf | 3 +- Avalonia.sln | 14 +- build/DevAnalyzers.props | 5 + samples/Directory.Build.props | 1 + src/tools/DevAnalyzers/DevAnalyzers.csproj | 4 +- src/tools/DevGenerators/DevGenerators.csproj | 4 +- .../PublicAnalyzers/Avalonia.Analyzers.csproj | 17 + ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 555 ++++++++++++++++++ .../AvaloniaPropertyAnalyzer.cs | 253 ++++++++ 9 files changed, 850 insertions(+), 6 deletions(-) create mode 100644 src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj create mode 100644 src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs create mode 100644 src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 741570061b..1d182b1357 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -8,9 +8,9 @@ "samples\\GpuInterop\\GpuInterop.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", @@ -42,6 +42,7 @@ "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.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 525e01c891..1e8ee85ffb 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,7 +231,14 @@ 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}") = "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}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" EndProject @@ -560,6 +567,10 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -626,6 +637,7 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 14e4f6a563..7d021d051f 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -5,5 +5,10 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0"/> + diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index 3b14f0ce12..ac78d9c739 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -6,4 +6,5 @@ 11 + diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index e5c2fc6cf6..2d9331b5dc 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 30da940514..7e63987d1b 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -7,11 +7,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj new file mode 100644 index 0000000000..31b8d08541 --- /dev/null +++ b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs new file mode 100644 index 0000000000..f11ed838a5 --- /dev/null +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -0,0 +1,555 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Avalonia.Analyzers; + +public partial class AvaloniaPropertyAnalyzer +{ + public class CompileAnalyzer + { + /// + /// A dictionary that maps field/property symbols to the AvaloniaProperty objects assigned to them. + /// + private readonly ConcurrentDictionary _avaloniaProperyDescriptions = new(SymbolEqualityComparer.Default); + + private readonly ConcurrentDictionary> _clrPropertyToAvaloniaProperties = new(SymbolEqualityComparer.Default); + + private readonly INamedTypeSymbol _avaloniaObjectType; + private readonly ImmutableHashSet _getValueMethods; + private readonly ImmutableHashSet _setValueMethods; + private readonly INamedTypeSymbol _avaloniaPropertyType; + private readonly INamedTypeSymbol _styledPropertyType; + private readonly INamedTypeSymbol _attachedPropertyType; + private readonly INamedTypeSymbol _directPropertyType; + private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; + private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; + + public CompileAnalyzer(CompilationStartAnalysisContext context) + { + _avaloniaObjectType = GetTypeOrThrow("Avalonia.AvaloniaObject"); + _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(SymbolEqualityComparer.Default); + _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(SymbolEqualityComparer.Default); + + _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); + _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); + _attachedPropertyType = GetTypeOrThrow("Avalonia.AttachedProperty`1"); + _directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2"); + + _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() + .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(SymbolEqualityComparer.Default); + + _avaloniaPropertyAddOwnerMethods = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType } + .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(SymbolEqualityComparer.Default); + + FindAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); + + context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); + context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); + + context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property); + + if (context.Compilation.Language == LanguageNames.CSharp) + { + context.RegisterCodeBlockAction(AnalyzePropertyMethods); + } + + INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context."); + } + + private void FindAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken) + { + var namespaceStack = new Stack(); + namespaceStack.Push(compilation.GlobalNamespace); + + var types = new List(); + + while (namespaceStack.Count > 0) + { + var current = namespaceStack.Pop(); + + foreach (var type in current.GetTypeMembers()) + { + if (DerivesFrom(type, _avaloniaObjectType)) + { + types.Add(type); + } + } + + foreach (var child in current.GetNamespaceMembers()) + { + namespaceStack.Push(child); + } + } + + var references = new ConcurrentBag<(ISymbol symbol, Func> getInits)>(); + + var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; + + Parallel.ForEach(types, parallelOptions, type => + { + foreach (var member in type.GetMembers()) + { + switch (member) + { + case IFieldSymbol fieldSymbol when IsValidAvaloniaPropertyStorage(fieldSymbol): + references.Add((fieldSymbol, so => ((IFieldInitializerOperation)so).InitializedFields)); + break; + case IPropertySymbol propertySymbol when IsValidAvaloniaPropertyStorage(propertySymbol): + references.Add((propertySymbol, so => ((IPropertyInitializerOperation)so).InitializedProperties)); + break; + } + } + }); + + // key initializes value + var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default); + + Parallel.ForEach(references, parallelOptions, tuple => + { + foreach (var syntaxRef in tuple.symbol.DeclaringSyntaxReferences) + { + var node = syntaxRef.GetSyntax(cancellationToken); + if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) + { + continue; + } + + var model = compilation.GetSemanticModel(node.SyntaxTree); + var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType().FirstOrDefault(); + + if (operation == null) + { + return; + } + + var operationValue = operation.Value; + + while (operationValue is IConversionOperation conversion) + { + operationValue = conversion.Operand; + } + + switch (operationValue) + { + case IInvocationOperation invocation: + RegisterInitializer_Invocation(tuple.getInits(operation), invocation, tuple.symbol); + break; + case IFieldReferenceOperation fieldRef when IsValidAvaloniaPropertyStorage(fieldRef.Field): + fieldInitializations[fieldRef.Field] = tuple.symbol; + break; + case IPropertyReferenceOperation propRef when IsValidAvaloniaPropertyStorage(propRef.Property): + fieldInitializations[propRef.Property] = tuple.symbol; + break; + } + } + }); + + // we have recorded every Register and AddOwner call. Now follow assignment chains. + foreach (var root in fieldInitializations.Keys.Intersect(_avaloniaProperyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray()) + { + var propertyDescription = _avaloniaProperyDescriptions[root]; + var owner = propertyDescription.AssignedTo[root]; + + var current = root; + do + { + var target = fieldInitializations[current]; + + propertyDescription.AssignedTo[target] = owner; // This loop handles simple assignment operations, so do NOT change the owner + _avaloniaProperyDescriptions[target] = propertyDescription; + + fieldInitializations.TryGetValue(target, out current); + } + while(current != null); + } + } + + private void RegisterInitializer_Invocation(IEnumerable initializedSymbols, IInvocationOperation invocation, ISymbol target) + { + try + { + if (invocation.TargetMethod.ReturnType is not INamedTypeSymbol propertyType) + { + return; + } + + if (_avaloniaPropertyRegisterMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AvaloniaProperty.Register* methods + { + if (!invocation.TargetMethod.IsGenericMethod) + { + return; + } + + var typeParamLookup = invocation.TargetMethod.TypeParameters.Select((s, i) => (param: s, index: i)) + .ToDictionary(t => t.param.Name, t => (INamedTypeSymbol)invocation.TargetMethod.TypeArguments[t.index]); + + if (!typeParamLookup.TryGetValue("TOwner", out var ownerType) && // if it's NOT a generic parameter, try to work out the runtime value + invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is INamedTypeSymbol ownerTypeParam && + invocation.Arguments.FirstOrDefault(a => SymbolEquals(a.Parameter, ownerTypeParam)) is IArgumentOperation argument) + { + switch (argument.Value) + { + case ITypeOfOperation typeOf: + ownerType = (INamedTypeSymbol)typeOf.Type!; + break; + } + } + + if (ownerType == null || !typeParamLookup.TryGetValue("TValue", out var propertyValueType)) + { + return; + } + + foreach (var symbol in initializedSymbols) + { + string name; + switch (invocation.Arguments[0].Value) + { + case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference: + name = propertyReference.Property.Name; + break; + case IAssignmentOperation assignment when assignment.ConstantValue is { HasValue: true } stringLiteral: + name = (string)stringLiteral.Value!; + break; + default: + return; + } + + var description = _avaloniaProperyDescriptions.GetOrAdd(symbol, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueType)); + description.Name = name; + description.AssignedTo[symbol] = ownerType; + description.OwnerTypes.Add(ownerType); + } + } + else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods + { + if (invocation.TargetMethod.TypeArguments[0] is not INamedTypeSymbol ownerType) + { + return; + } + + ISymbol sourceSymbol; + switch (invocation.Instance) + { + case IFieldReferenceOperation fieldReference: + sourceSymbol = fieldReference.Field; + break; + case IPropertyReferenceOperation propertyReference: + sourceSymbol = propertyReference.Property; + break; + default: + return; + } + + var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); + + foreach (var symbol in initializedSymbols) + { + var description = _avaloniaProperyDescriptions.GetOrAdd(symbol, s => + { + string inferredName = target.Name; + + var match = Regex.Match(target.Name, "(?.*)Property$"); + if (match.Success) + { + inferredName = match.Groups["name"].Value; + } + return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType); + }); + + description.AssignedTo[symbol] = ownerType; + description.OwnerTypes.Add(ownerType); + } + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to register the initializer of '{target}'.", ex); + } + } + + private void AnalyzeFieldInitializer(OperationAnalysisContext context) + { + var operation = (IFieldInitializerOperation)context.Operation; + + foreach (var field in operation.InitializedFields) + { + try + { + if (!_avaloniaProperyDescriptions.TryGetValue(field, out var description)) + { + continue; + } + + if (!IsValidAvaloniaPropertyStorage(field)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, field.Locations[0], field)); + } + + AnalyzeInitializer_Shared(context, field, description); + + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process initialization of field '{field}'.", ex); + } + } + } + + private void AnalyzePropertyInitializer(OperationAnalysisContext context) + { + var operation = (IPropertyInitializerOperation)context.Operation; + + foreach (var property in operation.InitializedProperties) + { + try + { + if (!_avaloniaProperyDescriptions.TryGetValue(property, out var description)) + { + continue; + } + + if (!IsValidAvaloniaPropertyStorage(property)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, property.Locations[0], property)); + } + + AnalyzeInitializer_Shared(context, property, description); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process initialization of property '{property}'.", ex); + } + } + } + + private void AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description) + { + if (!assignmentSymbol.Name.Contains(description.Name)) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyNameMismatch, assignmentSymbol.Locations[0], + description.Name, assignmentSymbol)); + } + + try + { + var ownerType = description.AssignedTo[assignmentSymbol]; + + if (!IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && !SymbolEquals(ownerType, assignmentSymbol.ContainingType)) + { + context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, assignmentSymbol.Locations[0], ownerType)); + } + } + catch (KeyNotFoundException) + { + return; // WIP + throw new KeyNotFoundException($"Assignment operation for {assignmentSymbol} was not recorded."); + } + } + + private void StartPropertySymbolAnalysis(SymbolStartAnalysisContext context) + { + var property = (IPropertySymbol)context.Symbol; + try + { + var avaloniaPropertyDescriptions = GetAvaloniaPropertiesForType(property.ContainingType).ToLookup(d => d.Name); + + var candidateTargetProperties = avaloniaPropertyDescriptions[property.Name].ToImmutableArray(); + + switch (candidateTargetProperties.Length) + { + case 0: + return; // does not refer to an AvaloniaProperty + case 1: + candidateTargetProperties[0].PropertyWrappers.Add(property); + break; + } + + _clrPropertyToAvaloniaProperties[property] = candidateTargetProperties; + + context.RegisterSymbolEndAction(context => + { + if (candidateTargetProperties.Length > 1) + { + var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null); + context.ReportDiagnostic(Diagnostic.Create(AmbiguousPropertyName, property.Locations[0], candidateSymbols.SelectMany(s => s!.Locations), + property.ContainingType, property.Name, $"\n\t{string.Join("\n\t", candidateSymbols)}")); + return; + } + + var avaloniaPropertyDescription = candidateTargetProperties[0]; + var avaloniaPropertyStorage = avaloniaPropertyDescription.ClosestAssignmentFor(property.ContainingType); + + if (avaloniaPropertyStorage == null) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create(AssociatedAvaloniaProperty, property.Locations[0], new[] { avaloniaPropertyStorage.Locations[0] }, + avaloniaPropertyDescription.PropertyType.Name, avaloniaPropertyStorage)); + + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType, includeNullability: true)) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyTypeMismatch, property.Locations[0], + avaloniaPropertyStorage, $"\t\n{string.Join("\t\n", avaloniaPropertyDescription.ValueType, property.Type)}")); + } + + if (property.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, property.Locations[0], "property", avaloniaPropertyStorage)); + } + + VerifyAccessor(property.GetMethod, "readable", "get"); + + if (!IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + VerifyAccessor(property.SetMethod, "writeable", "set"); + } + + void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) + { + if (method == null) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } + else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); + } + } + }); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to analyse property '{property}'.", ex); + } + } + + private void AnalyzePropertyMethods(CodeBlockAnalysisContext context) + { + if (context.OwningSymbol is not IMethodSymbol { AssociatedSymbol: IPropertySymbol property } method) + { + return; + } + + try + { + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties) || + candidateTargetProperties.Length != 1) // a diagnostic about multiple candidates will have already been reported + { + return; + } + + var avaloniaPropertyDescription = candidateTargetProperties.Single(); + + if (IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + return; + } + + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType)) + { + return; // a diagnostic about this will have already been reported, and if the cast is implicit then this message would be confusing anyway + } + + var bodyNode = context.CodeBlock.ChildNodes().Single(); + + var operation = bodyNode.DescendantNodes() + .Where(n => n.IsKind(SyntaxKind.InvocationExpression)) // this line is specific to C# + .Select(n => (IInvocationOperation)context.SemanticModel.GetOperation(n)!) + .FirstOrDefault(); + + var isGetMethod = method.MethodKind == MethodKind.PropertyGet; + + var expectedInvocations = isGetMethod ? _getValueMethods : _setValueMethods; + + if (operation == null || bodyNode.ChildNodes().Count() != 1 || !expectedInvocations.Contains(operation.TargetMethod.OriginalDefinition)) + { + ReportSideEffects(); + return; + } + + if (operation.Arguments.Length != 0) + { + var argumentValue = operation.Arguments[0].Value; + if (argumentValue is IConversionOperation conversion) + { + argumentValue = conversion.Operand; + } + + switch (argumentValue) + { + case IFieldReferenceOperation fieldRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(fieldRef.Field): + case IPropertyReferenceOperation propertyRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(propertyRef.Property): + break; // the argument is a reference to the correct AvaloniaProperty object + default: + ReportSideEffects(argumentValue.Syntax.GetLocation()); + return; + } + } + + if (!isGetMethod && + operation.Arguments.Length >= 2 && + operation.Arguments[1].Value.Kind != OperationKind.ParameterReference) // passing something other than `value` to SetValue + { + ReportSideEffects(operation.Arguments[1].Syntax.GetLocation()); + } + + void ReportSideEffects(Location? locationOverride = null) + { + var propertySourceName = avaloniaPropertyDescription.ClosestAssignmentFor(method.ContainingType)?.Name ?? "[unknown]"; + + context.ReportDiagnostic(Diagnostic.Create(AccessorSideEffects, locationOverride ?? context.CodeBlock.GetLocation(), + avaloniaPropertyDescription.Name, + isGetMethod ? "read" : "written to", + isGetMethod ? "get" : "set", + isGetMethod ? $"GetValue({propertySourceName})" : $"SetValue({propertySourceName}, value)")); + } + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process property accessor '{method}'.", ex); + } + } + + private INamedTypeSymbol AvaloniaPropertyType_GetValueType(INamedTypeSymbol type) + { + var compareType = type.IsGenericType ? type.ConstructUnboundGenericType().OriginalDefinition : type; + + if (SymbolEquals(compareType, _styledPropertyType) || SymbolEquals(compareType, _attachedPropertyType)) + { + return (INamedTypeSymbol)type.TypeArguments[0]; + } + else if (SymbolEquals(compareType, _directPropertyType)) + { + return (INamedTypeSymbol)type.TypeArguments[1]; + } + + throw new ArgumentException($"{type} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType}).", nameof(type)); + } + + private ImmutableHashSet GetAvaloniaPropertiesForType(ITypeSymbol type) + { + var properties = new List(); + + var current = type; + while (current != null) + { + properties.AddRange(current.GetMembers().Intersect(_avaloniaProperyDescriptions.Keys, SymbolEqualityComparer.Default).Select(s => _avaloniaProperyDescriptions[s])); + current = current.BaseType; + } + + return properties.ToImmutableHashSet(); + } + } +} diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs new file mode 100644 index 0000000000..e54ac397fb --- /dev/null +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Avalonia.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer +{ + private const string Category = "AvaloniaProperty"; + + private const string TypeMismatchTag = "TypeMismatch"; + private const string NameCollisionTag = "NameCollision"; + private const string AssociatedPropertyTag = "AssociatedProperty"; + + private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new( + "AVP0001", + "Identify the AvaloniaProperty associated with a CLR property", + "Associated AvaloniaProperty: {0} {1}", + Category, + DiagnosticSeverity.Info, + isEnabledByDefault: false, + "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.", + AssociatedPropertyTag); + + private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( + "AVP1000", + "Store AvaloniaProperty objects appropriately", + "Incorrect AvaloniaProperty storage: {0} should be static and readonly", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "AvaloniaProperty objects have static lifetimes and should be stored accordingly. Do not multiply construct the same property."); + + private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( + "AVP1010", + "Avaloniaproperty objects should declare their owner to be the type in which they are stored", + "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.", + TypeMismatchTag); + + private static readonly DiagnosticDescriptor DuplicatePropertyName = new( + "AVP1020", + "AvaloniaProperty names should be unique within each class", + "Name collision: {0} has the same name as {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Querying for an AvaloniaProperty by name requires that each property associated with a type have a unique name.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor AmbiguousPropertyName = new( + "AVP1021", + "Ensure an umabiguous relationship between CLR properties and Avalonia properties within the same class", + "Name collision: {0} owns multiple Avalonia properties with the name '{1}' {2}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its AddOwner method and provide new metadata.", + NameCollisionTag); + + private static readonly DiagnosticDescriptor PropertyNameMismatch = new( + "AVP1022", + "Store each AvaloniaProperty object in a field or CLR property which reflects its name", + "Bad name: An AvaloniaProperty named '{0}' is being assigned to {1}. These names do not relate.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".", + NameCollisionTag); + + private static readonly DiagnosticDescriptor AccessorSideEffects = new( + "AVP1030", + "Do not add side effects to StyledProperty accessors", + "Side effects: '{0}' is an AvaloniaProperty which can be {1} without the use of this CLR property. This {2} accessor should do nothing except call {3}.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, create a Coerce method or a PropertyChanged subscriber.", + AssociatedPropertyTag); + + private static readonly DiagnosticDescriptor MissingAccessor = new( + "AVP1031", + "A CLR property should support the same get/set operations as its associated AvaloniaProperty", + "Missing accessor: {0} is {1}, but this CLR property lacks a {2} accessor", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.", + AssociatedPropertyTag); + + private static readonly DiagnosticDescriptor InconsistentAccessibility = new( + "AVP1032", + "A CLR property and its accessors should be equally accessible as its associated AvaloniaProperty", + "Inconsistent accessibility: CLR {0} accessiblity does not match accessibility of {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different acessibility from its associated AvaloniaProperty is ineffective.", + AssociatedPropertyTag); + + private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( + "AVP1040", + "CLR property type should match associated AvaloniaProperty type", + "Type mismatch: CLR property type differs from the value type of {0} {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.", + TypeMismatchTag, AssociatedPropertyTag); + + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + AssociatedAvaloniaProperty, + InappropriatePropertyAssignment, + OwnerDoesNotMatchOuterType, + DuplicatePropertyName, + AmbiguousPropertyName, + PropertyNameMismatch, + AccessorSideEffects, + MissingAccessor, + InconsistentAccessibility, + PropertyTypeMismatch); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(c => new CompileAnalyzer(c)); + } + + private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, params INamedTypeSymbol[] propertyTypes) + { + if (type.IsGenericType) + { + type = type.ConstructUnboundGenericType().OriginalDefinition; + } + + return propertyTypes.Any(t => SymbolEquals(type, t)); + } + + private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol baseType) + { + while (type != null) + { + if (SymbolEquals(type, baseType)) + { + return true; + } + + type = type.BaseType; + } + + return false; + } + + private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; + private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; + + private static bool SymbolEquals(ISymbol? x, ISymbol? y, bool includeNullability = false) + { + // The current version of Microsoft.CodeAnalysis includes an "IncludeNullability" comparer, + // but it overshoots the target and tries to compare EVERYTHING. This leads to two symbols for + // the same type not being equal if they were imported into different compile units (i.e. assemblies). + // So for now, we will just discard this parameter. + _ = includeNullability; + + return SymbolEqualityComparer.Default.Equals(x, y); + } + + private class AvaloniaPropertyDescription + { + /// + /// Gets the name that was assigned to this property when it was registered. + /// + /// + /// If the property was not registered within the current compile context, this value will be inferred from + /// the name of the field (or CLR property) in which the AvaloniaProperty object is stored. + /// + public string Name { get; set; } + + /// + /// Gets the type of the AvaloniaProperty itself: Styled, Direct, or Attached + /// + public INamedTypeSymbol PropertyType { get; } + + /// + /// Gets the TValue type that the property stores. + /// + public INamedTypeSymbol ValueType { get; } + + /// + /// Gets the type which registered the property, and all types which have added themselves as owners. + /// + public ConcurrentBag OwnerTypes { get; } = new(); + + /// + /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. + /// + public ConcurrentDictionary AssignedTo { get; } = new(SymbolEqualityComparer.Default); + + /// + /// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject. + /// + public ConcurrentBag PropertyWrappers { get; } = new(); + + public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, INamedTypeSymbol valueType) + { + Name = name; + PropertyType = propertyType; + ValueType = valueType; + } + + /// + /// Searches the inheritance hierarchy of the given type for a field or property to which this AvaloniaProperty is assigned. + /// + public ISymbol? ClosestAssignmentFor(ITypeSymbol? type) + { + var assignmentsByType = AssignedTo.Keys.ToLookup(s => s.ContainingType, SymbolEqualityComparer.Default); + + while (type != null) + { + if (assignmentsByType.Contains(type)) + { + return assignmentsByType[type].First(); + } + type = type.BaseType; + } + + return null; + } + } + +} + +[Serializable] +public class AvaloniaAnalysisException : Exception +{ + public AvaloniaAnalysisException(string message, Exception? innerException = null) : base(message, innerException) + { + } + + protected AvaloniaAnalysisException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +} From df9a80c86675ce1eed496121dc7e31f6a7a47665 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Wed, 1 Feb 2023 22:07:38 +0100 Subject: [PATCH 24/60] Detect AvaloniaProperties in static constructors, and with string literal names Follow conversion and assignment operations back to their source Narrowed the report location of several diagnostics --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 270 ++++++++++++------ .../AvaloniaPropertyAnalyzer.cs | 42 ++- 2 files changed, 222 insertions(+), 90 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index f11ed838a5..3c4d5360f5 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -24,6 +24,7 @@ public partial class AvaloniaPropertyAnalyzer private readonly ConcurrentDictionary> _clrPropertyToAvaloniaProperties = new(SymbolEqualityComparer.Default); + private readonly INamedTypeSymbol _stringType; private readonly INamedTypeSymbol _avaloniaObjectType; private readonly ImmutableHashSet _getValueMethods; private readonly ImmutableHashSet _setValueMethods; @@ -31,11 +32,13 @@ public partial class AvaloniaPropertyAnalyzer private readonly INamedTypeSymbol _styledPropertyType; private readonly INamedTypeSymbol _attachedPropertyType; private readonly INamedTypeSymbol _directPropertyType; + private readonly ImmutableArray _allAvaloniaPropertyTypes; private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; public CompileAnalyzer(CompilationStartAnalysisContext context) { + _stringType = GetTypeOrThrow("System.String"); _avaloniaObjectType = GetTypeOrThrow("Avalonia.AvaloniaObject"); _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(SymbolEqualityComparer.Default); _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(SymbolEqualityComparer.Default); @@ -48,13 +51,16 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(SymbolEqualityComparer.Default); - _avaloniaPropertyAddOwnerMethods = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType } + _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableArray(); + + _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(SymbolEqualityComparer.Default); FindAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); + context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property); @@ -66,6 +72,9 @@ public partial class AvaloniaPropertyAnalyzer INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context."); } + private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); + private bool IsAvaloniaPropertyStorage(IPropertySymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); + private void FindAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken) { var namespaceStack = new Stack(); @@ -91,32 +100,78 @@ public partial class AvaloniaPropertyAnalyzer } } - var references = new ConcurrentBag<(ISymbol symbol, Func> getInits)>(); + var avaloniaPropertyStorageSymbols = new ConcurrentBag(); + + // key initializes value + var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default); var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; Parallel.ForEach(types, parallelOptions, type => { - foreach (var member in type.GetMembers()) + try { - switch (member) + foreach (var member in type.GetMembers()) { - case IFieldSymbol fieldSymbol when IsValidAvaloniaPropertyStorage(fieldSymbol): - references.Add((fieldSymbol, so => ((IFieldInitializerOperation)so).InitializedFields)); - break; - case IPropertySymbol propertySymbol when IsValidAvaloniaPropertyStorage(propertySymbol): - references.Add((propertySymbol, so => ((IPropertyInitializerOperation)so).InitializedProperties)); - break; + switch (member) + { + case IFieldSymbol fieldSymbol when IsAvaloniaPropertyStorage(fieldSymbol): + avaloniaPropertyStorageSymbols.Add(fieldSymbol); + break; + case IPropertySymbol propertySymbol when IsAvaloniaPropertyStorage(propertySymbol): + avaloniaPropertyStorageSymbols.Add(propertySymbol); + break; + } + } + + foreach (var constructor in type.StaticConstructors) + { + foreach (var syntaxRef in constructor.DeclaringSyntaxReferences) + { + var node = syntaxRef.GetSyntax(cancellationToken); + if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) + { + continue; + } + + var model = compilation.GetSemanticModel(node.SyntaxTree); + + foreach (var descendant in node.DescendantNodes()) + { + switch (descendant.Kind()) + { + case SyntaxKind.SimpleAssignmentExpression: + var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!; + + var target = assignmentOperation.Target switch + { + IFieldReferenceOperation fieldRef => fieldRef.Field, + IPropertyReferenceOperation propertyRef => propertyRef.Property, + _ => default(ISymbol), + }; + + if (target == null) + { + break; + } + + RegisterAssignment(target, assignmentOperation.Value); + + break; + } + } + } } } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw new AvaloniaAnalysisException($"Failed to find AvaloniaProperty objects in {type}.", ex); + } }); - // key initializes value - var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default); - - Parallel.ForEach(references, parallelOptions, tuple => + Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol => { - foreach (var syntaxRef in tuple.symbol.DeclaringSyntaxReferences) + foreach (var syntaxRef in symbol.DeclaringSyntaxReferences) { var node = syntaxRef.GetSyntax(cancellationToken); if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) @@ -132,25 +187,7 @@ public partial class AvaloniaPropertyAnalyzer return; } - var operationValue = operation.Value; - - while (operationValue is IConversionOperation conversion) - { - operationValue = conversion.Operand; - } - - switch (operationValue) - { - case IInvocationOperation invocation: - RegisterInitializer_Invocation(tuple.getInits(operation), invocation, tuple.symbol); - break; - case IFieldReferenceOperation fieldRef when IsValidAvaloniaPropertyStorage(fieldRef.Field): - fieldInitializations[fieldRef.Field] = tuple.symbol; - break; - case IPropertyReferenceOperation propRef when IsValidAvaloniaPropertyStorage(propRef.Property): - fieldInitializations[propRef.Property] = tuple.symbol; - break; - } + RegisterAssignment(symbol, operation.Value); } }); @@ -170,11 +207,27 @@ public partial class AvaloniaPropertyAnalyzer fieldInitializations.TryGetValue(target, out current); } - while(current != null); + while (current != null); + } + + void RegisterAssignment(ISymbol target, IOperation value) + { + switch (ResolveOperationSource(value)) + { + case IInvocationOperation invocation: + RegisterInitializer_Invocation(invocation, target); + break; + case IFieldReferenceOperation fieldRef when IsAvaloniaPropertyStorage(fieldRef.Field): + fieldInitializations[fieldRef.Field] = target; + break; + case IPropertyReferenceOperation propRef when IsAvaloniaPropertyStorage(propRef.Property): + fieldInitializations[propRef.Property] = target; + break; + } } } - private void RegisterInitializer_Invocation(IEnumerable initializedSymbols, IInvocationOperation invocation, ISymbol target) + private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target) { try { @@ -191,45 +244,65 @@ public partial class AvaloniaPropertyAnalyzer } var typeParamLookup = invocation.TargetMethod.TypeParameters.Select((s, i) => (param: s, index: i)) - .ToDictionary(t => t.param.Name, t => (INamedTypeSymbol)invocation.TargetMethod.TypeArguments[t.index]); + .ToDictionary(t => t.param.Name, t => + { + var argument = invocation.TargetMethod.TypeArguments[t.index]; + + var typeArgumentSyntax = invocation.Syntax; + if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them + { + try + { + typeArgumentSyntax = invocation.Syntax.DescendantNodes() + .First(n => n.IsKind(SyntaxKind.TypeArgumentList)) + .DescendantNodes().ElementAt(t.index); + } + catch + { + // ignore, this is unimportant + } + } + + return new TypeReference((INamedTypeSymbol)argument, typeArgumentSyntax.GetLocation()); + }); - if (!typeParamLookup.TryGetValue("TOwner", out var ownerType) && // if it's NOT a generic parameter, try to work out the runtime value + if (!typeParamLookup.TryGetValue("TOwner", out var ownerTypeRef) && // if it's NOT a generic parameter, try to work out the runtime value invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is INamedTypeSymbol ownerTypeParam && invocation.Arguments.FirstOrDefault(a => SymbolEquals(a.Parameter, ownerTypeParam)) is IArgumentOperation argument) { - switch (argument.Value) + switch (ResolveOperationSource(argument.Value)) { - case ITypeOfOperation typeOf: - ownerType = (INamedTypeSymbol)typeOf.Type!; + case ITypeOfOperation typeOf when typeOf.Type is INamedTypeSymbol type: + ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); break; } } - if (ownerType == null || !typeParamLookup.TryGetValue("TValue", out var propertyValueType)) + if (ownerTypeRef.Type == null || !typeParamLookup.TryGetValue("TValue", out var propertyValueTypeRef)) { return; } - foreach (var symbol in initializedSymbols) + string name; + switch (ResolveOperationSource(invocation.Arguments[0].Value)) { - string name; - switch (invocation.Arguments[0].Value) - { - case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference: - name = propertyReference.Property.Name; - break; - case IAssignmentOperation assignment when assignment.ConstantValue is { HasValue: true } stringLiteral: - name = (string)stringLiteral.Value!; - break; - default: - return; - } - - var description = _avaloniaProperyDescriptions.GetOrAdd(symbol, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueType)); - description.Name = name; - description.AssignedTo[symbol] = ownerType; - description.OwnerTypes.Add(ownerType); + case ILiteralOperation literal when SymbolEquals(literal.Type, _stringType): + name = (string)literal.ConstantValue.Value!; + break; + case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference: + name = propertyReference.Property.Name; + break; + case IFieldReferenceOperation fieldRef when SymbolEquals(fieldRef.Type, _stringType) && fieldRef.ConstantValue is { HasValue: true } constantValue: + name = (string) fieldRef.ConstantValue.Value!; + break; + default: + return; } + + var description = _avaloniaProperyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueTypeRef.Type)); + description.Name = name; + description.AssignedTo[target] = ownerTypeRef; + description.OwnerTypes.Add(ownerTypeRef); } else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods { @@ -238,6 +311,8 @@ public partial class AvaloniaPropertyAnalyzer return; } + var ownerTypeRef = new TypeReference(ownerType, invocation.TargetMethod.TypeArguments[0].Locations[0]); + ISymbol sourceSymbol; switch (invocation.Instance) { @@ -253,23 +328,20 @@ public partial class AvaloniaPropertyAnalyzer var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); - foreach (var symbol in initializedSymbols) + var description = _avaloniaProperyDescriptions.GetOrAdd(target, s => { - var description = _avaloniaProperyDescriptions.GetOrAdd(symbol, s => - { - string inferredName = target.Name; + string inferredName = target.Name; - var match = Regex.Match(target.Name, "(?.*)Property$"); - if (match.Success) - { - inferredName = match.Groups["name"].Value; - } - return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType); - }); + var match = Regex.Match(target.Name, "(?.*)Property$"); + if (match.Success) + { + inferredName = match.Groups["name"].Value; + } + return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType); + }); - description.AssignedTo[symbol] = ownerType; - description.OwnerTypes.Add(ownerType); - } + description.AssignedTo[target] = ownerTypeRef; + description.OwnerTypes.Add(ownerTypeRef); } } catch (Exception ex) @@ -333,6 +405,37 @@ public partial class AvaloniaPropertyAnalyzer } } + private void AnalyzeAssignment(OperationAnalysisContext context) + { + var operation = (IAssignmentOperation)context.Operation; + + try + { + var (target, isValid) = operation.Target switch + { + IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), + IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), + _ => (default(ISymbol), false), + }; + + if (target == null || !_avaloniaProperyDescriptions.TryGetValue(target, out var description)) + { + return; + } + + if (!isValid) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, target.Locations[0], target)); + } + + AnalyzeInitializer_Shared(context, target, description); + } + catch (Exception ex) + { + throw new AvaloniaAnalysisException($"Failed to process assignment '{operation}'.", ex); + } + } + private void AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description) { if (!assignmentSymbol.Name.Contains(description.Name)) @@ -345,14 +448,13 @@ public partial class AvaloniaPropertyAnalyzer { var ownerType = description.AssignedTo[assignmentSymbol]; - if (!IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && !SymbolEquals(ownerType, assignmentSymbol.ContainingType)) + if (!IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType)) { - context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, assignmentSymbol.Locations[0], ownerType)); + context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, ownerType.Location, ownerType.Type)); } } catch (KeyNotFoundException) { - return; // WIP throw new KeyNotFoundException($"Assignment operation for {assignmentSymbol} was not recorded."); } } @@ -444,7 +546,7 @@ public partial class AvaloniaPropertyAnalyzer try { - if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties) || + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties) || candidateTargetProperties.Length != 1) // a diagnostic about multiple candidates will have already been reported { return; @@ -463,7 +565,7 @@ public partial class AvaloniaPropertyAnalyzer } var bodyNode = context.CodeBlock.ChildNodes().Single(); - + var operation = bodyNode.DescendantNodes() .Where(n => n.IsKind(SyntaxKind.InvocationExpression)) // this line is specific to C# .Select(n => (IInvocationOperation)context.SemanticModel.GetOperation(n)!) @@ -481,19 +583,13 @@ public partial class AvaloniaPropertyAnalyzer if (operation.Arguments.Length != 0) { - var argumentValue = operation.Arguments[0].Value; - if (argumentValue is IConversionOperation conversion) - { - argumentValue = conversion.Operand; - } - - switch (argumentValue) + switch (ResolveOperationSource(operation.Arguments[0].Value)) { case IFieldReferenceOperation fieldRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(fieldRef.Field): case IPropertyReferenceOperation propertyRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(propertyRef.Property): break; // the argument is a reference to the correct AvaloniaProperty object default: - ReportSideEffects(argumentValue.Syntax.GetLocation()); + ReportSideEffects(operation.Arguments[0].Value.Syntax.GetLocation()); return; } } diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index e54ac397fb..aeaeb72e07 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Runtime.Serialization; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; namespace Avalonia.Analyzers; @@ -136,7 +138,9 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer context.RegisterCompilationStartAction(c => new CompileAnalyzer(c)); } - private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, params INamedTypeSymbol[] propertyTypes) + private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable()); + + private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, IEnumerable propertyTypes) { if (type.IsGenericType) { @@ -161,6 +165,27 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer return false; } + /// + /// Follows assignments and conversions back to their source. + /// + private static IOperation ResolveOperationSource(IOperation operation) + { + while (true) + { + switch (operation) + { + case IConversionOperation conversion: + operation = conversion.Operand; + break; + case ISimpleAssignmentOperation assignment: + operation = assignment.Value; + break; + default: + return operation; + } + } + } + private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; @@ -199,12 +224,12 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer /// /// Gets the type which registered the property, and all types which have added themselves as owners. /// - public ConcurrentBag OwnerTypes { get; } = new(); + public ConcurrentBag OwnerTypes { get; } = new(); /// /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. /// - public ConcurrentDictionary AssignedTo { get; } = new(SymbolEqualityComparer.Default); + public ConcurrentDictionary AssignedTo { get; } = new(SymbolEqualityComparer.Default); /// /// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject. @@ -238,6 +263,17 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer } } + private readonly struct TypeReference + { + public INamedTypeSymbol Type { get; } + public Location Location { get; } + + public TypeReference(INamedTypeSymbol type, Location location) + { + Type = type; + Location = location; + } + } } [Serializable] From d78bcfaa394e76eaab03c1da10675e5d5d84908a Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Fri, 3 Feb 2023 20:45:05 +0100 Subject: [PATCH 25/60] Optimisation: convert all collections to immutable forms at the end of FindAvaloniaPropertySymbols Optimisation: move CLR property wrapper detection into FindAvaloniaPropertySymbols Don't start analysis if we can't locate AvaloniaObject --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 118 ++++++++++-------- .../AvaloniaPropertyAnalyzer.cs | 57 ++++++++- 2 files changed, 122 insertions(+), 53 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 3c4d5360f5..affea895db 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -20,9 +20,12 @@ public partial class AvaloniaPropertyAnalyzer /// /// A dictionary that maps field/property symbols to the AvaloniaProperty objects assigned to them. /// - private readonly ConcurrentDictionary _avaloniaProperyDescriptions = new(SymbolEqualityComparer.Default); + private ImmutableDictionary _avaloniaPropertyDescriptions = null!; - private readonly ConcurrentDictionary> _clrPropertyToAvaloniaProperties = new(SymbolEqualityComparer.Default); + /// + /// Maps properties onto all AvaloniaProperty objects that they may be intended to represent. + /// + private ImmutableDictionary> _clrPropertyToAvaloniaProperties = null!; private readonly INamedTypeSymbol _stringType; private readonly INamedTypeSymbol _avaloniaObjectType; @@ -36,12 +39,14 @@ public partial class AvaloniaPropertyAnalyzer private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; - public CompileAnalyzer(CompilationStartAnalysisContext context) + public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) { + var methodComparer = SymbolEqualityComparer.Default; + _stringType = GetTypeOrThrow("System.String"); - _avaloniaObjectType = GetTypeOrThrow("Avalonia.AvaloniaObject"); - _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(SymbolEqualityComparer.Default); - _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(SymbolEqualityComparer.Default); + _avaloniaObjectType = avaloniaObjectType; + _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(methodComparer); + _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(methodComparer); _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); @@ -49,12 +54,12 @@ public partial class AvaloniaPropertyAnalyzer _directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2"); _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() - .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(SymbolEqualityComparer.Default); + .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(methodComparer); _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableArray(); _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes - .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(SymbolEqualityComparer.Default); + .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); FindAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); @@ -102,6 +107,8 @@ public partial class AvaloniaPropertyAnalyzer var avaloniaPropertyStorageSymbols = new ConcurrentBag(); + var propertyDescriptions = new ConcurrentDictionary(SymbolEqualityComparer.Default); + // key initializes value var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default); @@ -192,9 +199,9 @@ public partial class AvaloniaPropertyAnalyzer }); // we have recorded every Register and AddOwner call. Now follow assignment chains. - foreach (var root in fieldInitializations.Keys.Intersect(_avaloniaProperyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray()) + foreach (var root in fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray()) { - var propertyDescription = _avaloniaProperyDescriptions[root]; + var propertyDescription = propertyDescriptions[root]; var owner = propertyDescription.AssignedTo[root]; var current = root; @@ -202,20 +209,57 @@ public partial class AvaloniaPropertyAnalyzer { var target = fieldInitializations[current]; - propertyDescription.AssignedTo[target] = owner; // This loop handles simple assignment operations, so do NOT change the owner - _avaloniaProperyDescriptions[target] = propertyDescription; + propertyDescription.AddAssignment(target, owner); // This loop handles simple assignment operations, so do NOT change the owner + propertyDescriptions[target] = propertyDescription; fieldInitializations.TryGetValue(target, out current); } while (current != null); } + var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>(); + + var propertyDescriptionsByName = propertyDescriptions.Values.ToLookup(p => p.Name, p => (property: p, owners: p.OwnerTypes.Select(t => t.Type).ToImmutableHashSet(SymbolEqualityComparer.Default))); + + // Detect CLR properties that provide syntatic wrapping around an AvaloniaProperty (or potentially multiple, which leads to a warning diagnostic) + Parallel.ForEach(propertyDescriptions.Values, propertyDescription => + { + var nameMatches = propertyDescriptionsByName[propertyDescription.Name]; + + foreach (var ownerType in propertyDescription.OwnerTypes.Select(o => o.Type).Distinct(SymbolEqualityComparer.Default)) + { + if (ownerType.GetMembers(propertyDescription.Name).OfType().SingleOrDefault() is not { IsStatic: false } clrProperty) + { + continue; + } + + propertyDescription.AddPropertyWrapper(clrProperty); + clrPropertyWrapCandidates.Add((clrProperty, propertyDescription)); + + var current = ownerType.BaseType; + while (current != null) + { + foreach (var otherProp in nameMatches.Where(t => t.owners.Contains(current)).Select(t => t.property)) + { + clrPropertyWrapCandidates.Add((clrProperty, otherProp)); + } + + current = current.BaseType; + } + } + }); + + // convert our dictionaries to immutable form + _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer.Default) + .ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.ToImmutableArray(), SymbolEqualityComparer.Default); + _avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default); + void RegisterAssignment(ISymbol target, IOperation value) { switch (ResolveOperationSource(value)) { case IInvocationOperation invocation: - RegisterInitializer_Invocation(invocation, target); + RegisterInitializer_Invocation(invocation, target, propertyDescriptions); break; case IFieldReferenceOperation fieldRef when IsAvaloniaPropertyStorage(fieldRef.Field): fieldInitializations[fieldRef.Field] = target; @@ -227,7 +271,7 @@ public partial class AvaloniaPropertyAnalyzer } } - private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target) + private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary propertyDescriptions) { try { @@ -293,16 +337,16 @@ public partial class AvaloniaPropertyAnalyzer name = propertyReference.Property.Name; break; case IFieldReferenceOperation fieldRef when SymbolEquals(fieldRef.Type, _stringType) && fieldRef.ConstantValue is { HasValue: true } constantValue: - name = (string) fieldRef.ConstantValue.Value!; + name = (string)fieldRef.ConstantValue.Value!; break; default: return; } - var description = _avaloniaProperyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueTypeRef.Type)); + var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueTypeRef.Type)); description.Name = name; - description.AssignedTo[target] = ownerTypeRef; - description.OwnerTypes.Add(ownerTypeRef); + description.AddAssignment(target, ownerTypeRef); + description.AddOwner(ownerTypeRef); } else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods { @@ -328,7 +372,7 @@ public partial class AvaloniaPropertyAnalyzer var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); - var description = _avaloniaProperyDescriptions.GetOrAdd(target, s => + var description = propertyDescriptions.GetOrAdd(target, s => { string inferredName = target.Name; @@ -340,8 +384,8 @@ public partial class AvaloniaPropertyAnalyzer return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType); }); - description.AssignedTo[target] = ownerTypeRef; - description.OwnerTypes.Add(ownerTypeRef); + description.AddAssignment(target, ownerTypeRef); + description.AddOwner(ownerTypeRef); } } catch (Exception ex) @@ -358,7 +402,7 @@ public partial class AvaloniaPropertyAnalyzer { try { - if (!_avaloniaProperyDescriptions.TryGetValue(field, out var description)) + if (!_avaloniaPropertyDescriptions.TryGetValue(field, out var description)) { continue; } @@ -386,7 +430,7 @@ public partial class AvaloniaPropertyAnalyzer { try { - if (!_avaloniaProperyDescriptions.TryGetValue(property, out var description)) + if (!_avaloniaPropertyDescriptions.TryGetValue(property, out var description)) { continue; } @@ -418,7 +462,7 @@ public partial class AvaloniaPropertyAnalyzer _ => (default(ISymbol), false), }; - if (target == null || !_avaloniaProperyDescriptions.TryGetValue(target, out var description)) + if (target == null || !_avaloniaPropertyDescriptions.TryGetValue(target, out var description)) { return; } @@ -464,21 +508,11 @@ public partial class AvaloniaPropertyAnalyzer var property = (IPropertySymbol)context.Symbol; try { - var avaloniaPropertyDescriptions = GetAvaloniaPropertiesForType(property.ContainingType).ToLookup(d => d.Name); - - var candidateTargetProperties = avaloniaPropertyDescriptions[property.Name].ToImmutableArray(); - - switch (candidateTargetProperties.Length) + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) { - case 0: return; // does not refer to an AvaloniaProperty - case 1: - candidateTargetProperties[0].PropertyWrappers.Add(property); - break; } - _clrPropertyToAvaloniaProperties[property] = candidateTargetProperties; - context.RegisterSymbolEndAction(context => { if (candidateTargetProperties.Length > 1) @@ -633,19 +667,5 @@ public partial class AvaloniaPropertyAnalyzer throw new ArgumentException($"{type} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType}).", nameof(type)); } - - private ImmutableHashSet GetAvaloniaPropertiesForType(ITypeSymbol type) - { - var properties = new List(); - - var current = type; - while (current != null) - { - properties.AddRange(current.GetMembers().Intersect(_avaloniaProperyDescriptions.Keys, SymbolEqualityComparer.Default).Select(s => _avaloniaProperyDescriptions[s])); - current = current.BaseType; - } - - return properties.ToImmutableHashSet(); - } } } diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index aeaeb72e07..355fe34801 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Collections.ObjectModel; using System.Linq; using System.Runtime.Serialization; using Microsoft.CodeAnalysis; @@ -135,7 +136,13 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(c => new CompileAnalyzer(c)); + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName("Avalonia.AvaloniaObject") is { } avaloniaObjectType) + { + new CompileAnalyzer(c, avaloniaObjectType); + } + }); } private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable()); @@ -224,23 +231,57 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer /// /// Gets the type which registered the property, and all types which have added themselves as owners. /// - public ConcurrentBag OwnerTypes { get; } = new(); + public IReadOnlyCollection OwnerTypes { get; private set; } + private ConcurrentBag? _ownerTypes = new(); /// /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. /// - public ConcurrentDictionary AssignedTo { get; } = new(SymbolEqualityComparer.Default); + public IReadOnlyDictionary AssignedTo { get; private set; } + private ConcurrentDictionary? _assignedTo = new(SymbolEqualityComparer.Default); /// /// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject. /// - public ConcurrentBag PropertyWrappers { get; } = new(); + public IReadOnlyCollection PropertyWrappers { get; private set; } + private ConcurrentBag? _propertyWrappers = new(); public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, INamedTypeSymbol valueType) { Name = name; PropertyType = propertyType; ValueType = valueType; + + OwnerTypes = _ownerTypes; + PropertyWrappers = _propertyWrappers; + AssignedTo = _assignedTo; + } + + private const string SealedError = "PropertyDescription has been sealed."; + + public void AddOwner(TypeReference owner) => (_ownerTypes ?? throw new InvalidOperationException(SealedError)).Add(owner); + + public void AddPropertyWrapper(IPropertySymbol property) => (_propertyWrappers ?? throw new InvalidOperationException(SealedError)).Add(property); + + public void AddAssignment(ISymbol assignmentTarget, TypeReference ownerType) => (_assignedTo ?? throw new InvalidOperationException(SealedError)).TryAdd(assignmentTarget, ownerType); + + public AvaloniaPropertyDescription Seal() + { + if (_ownerTypes == null || _propertyWrappers == null || _assignedTo == null) + { + return this; + } + + OwnerTypes = _ownerTypes.ToImmutableHashSet(); + _ownerTypes = null; + + PropertyWrappers = _propertyWrappers.ToImmutableHashSet(SymbolEqualityComparer.Default); + _propertyWrappers = null; + + AssignedTo = new ReadOnlyDictionary(_assignedTo); + _assignedTo = null; + + return this; } /// @@ -274,6 +315,14 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Location = location; } } + + private class SymbolEqualityComparer : IEqualityComparer where T : ISymbol + { + public bool Equals(T x, T y) => SymbolEqualityComparer.Default.Equals(x, y); + public int GetHashCode(T obj) => SymbolEqualityComparer.Default.GetHashCode(obj); + + public static SymbolEqualityComparer Default { get; } = new(); + } } [Serializable] From ebcb512c83616714c4fff2a10fb0d43208079dd4 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Fri, 3 Feb 2023 20:53:27 +0100 Subject: [PATCH 26/60] Don't report property name mismatches if the field being assigned to is private --- .../AvaloniaPropertyAnalyzer.CompileAnalyzer.cs | 17 ++++++++++++++--- .../PublicAnalyzers/AvaloniaPropertyAnalyzer.cs | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index affea895db..018b1d6020 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -61,7 +61,7 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); - FindAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); + RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); @@ -80,7 +80,7 @@ public partial class AvaloniaPropertyAnalyzer private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); private bool IsAvaloniaPropertyStorage(IPropertySymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); - private void FindAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken) + private void RegisterAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken) { var namespaceStack = new Stack(); namespaceStack.Push(compilation.GlobalNamespace); @@ -394,6 +394,7 @@ public partial class AvaloniaPropertyAnalyzer } } + /// private void AnalyzeFieldInitializer(OperationAnalysisContext context) { var operation = (IFieldInitializerOperation)context.Operation; @@ -422,6 +423,7 @@ public partial class AvaloniaPropertyAnalyzer } } + /// private void AnalyzePropertyInitializer(OperationAnalysisContext context) { var operation = (IPropertyInitializerOperation)context.Operation; @@ -449,6 +451,7 @@ public partial class AvaloniaPropertyAnalyzer } } + /// private void AnalyzeAssignment(OperationAnalysisContext context) { var operation = (IAssignmentOperation)context.Operation; @@ -480,9 +483,11 @@ public partial class AvaloniaPropertyAnalyzer } } + /// + /// private void AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description) { - if (!assignmentSymbol.Name.Contains(description.Name)) + if (!assignmentSymbol.Name.Contains(description.Name) && assignmentSymbol.DeclaredAccessibility != Accessibility.Private) { context.ReportDiagnostic(Diagnostic.Create(PropertyNameMismatch, assignmentSymbol.Locations[0], description.Name, assignmentSymbol)); @@ -503,6 +508,11 @@ public partial class AvaloniaPropertyAnalyzer } } + /// + /// + /// + /// + /// private void StartPropertySymbolAnalysis(SymbolStartAnalysisContext context) { var property = (IPropertySymbol)context.Symbol; @@ -571,6 +581,7 @@ public partial class AvaloniaPropertyAnalyzer } } + /// private void AnalyzePropertyMethods(CodeBlockAnalysisContext context) { if (context.OwningSymbol is not IMethodSymbol { AssociatedSymbol: IPropertySymbol property } method) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index 355fe34801..0a59f43d6a 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -76,7 +76,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, - "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".", + "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".\nPrivate symbols are exempt from this diagnostic.", NameCollisionTag); private static readonly DiagnosticDescriptor AccessorSideEffects = new( From 2b68e42e31a2e92bae7e12f01931317f0d652d95 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sat, 4 Feb 2023 13:07:15 +0100 Subject: [PATCH 27/60] Added InappropriatePropertyRegistration and UnexpectedPropertyAccess Fixed AttachedProperty registrations in non-AvaloniaObject types not being detected Optimised parameter lookup --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 259 +++++++++++------- .../AvaloniaPropertyAnalyzer.cs | 98 ++++++- 2 files changed, 249 insertions(+), 108 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 018b1d6020..179105a1f5 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -31,6 +31,7 @@ public partial class AvaloniaPropertyAnalyzer private readonly INamedTypeSymbol _avaloniaObjectType; private readonly ImmutableHashSet _getValueMethods; private readonly ImmutableHashSet _setValueMethods; + private readonly ImmutableHashSet _allGetSetMethods; private readonly INamedTypeSymbol _avaloniaPropertyType; private readonly INamedTypeSymbol _styledPropertyType; private readonly INamedTypeSymbol _attachedPropertyType; @@ -38,6 +39,11 @@ public partial class AvaloniaPropertyAnalyzer private readonly ImmutableArray _allAvaloniaPropertyTypes; private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; + private readonly ImmutableHashSet _allAvaloniaPropertyMethods; + private readonly ImmutableDictionary _ownerTypeParams; + private readonly ImmutableDictionary _valueTypeParams; + private readonly ImmutableDictionary _hostTypeParams; + private readonly ImmutableDictionary _inheritsParams; public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) { @@ -47,6 +53,7 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaObjectType = avaloniaObjectType; _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(methodComparer); _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(methodComparer); + _allGetSetMethods = _getValueMethods.Concat(_setValueMethods).ToImmutableHashSet(methodComparer); _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); @@ -61,11 +68,19 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); + _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(SymbolEqualityComparer.Default); + + _ownerTypeParams = GetParamDictionary("TOwner", m => m.TypeParameters); + _valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters); + _hostTypeParams = GetParamDictionary("THost", m => m.TypeParameters); + _inheritsParams = GetParamDictionary("inherits", m => m.Parameters); + RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation); context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property); @@ -75,6 +90,11 @@ public partial class AvaloniaPropertyAnalyzer } INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context."); + + ImmutableDictionary GetParamDictionary(string name, Func> methodSymbolSelector) where TSymbol : ISymbol => _allAvaloniaPropertyMethods + .Select(m => methodSymbolSelector(m).SingleOrDefault(p => p.Name == name)) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => (IMethodSymbol)p.ContainingSymbol, SymbolEqualityComparer.Default); } private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); @@ -91,13 +111,7 @@ public partial class AvaloniaPropertyAnalyzer { var current = namespaceStack.Pop(); - foreach (var type in current.GetTypeMembers()) - { - if (DerivesFrom(type, _avaloniaObjectType)) - { - types.Add(type); - } - } + types.AddRange(current.GetTypeMembers()); foreach (var child in current.GetNamespaceMembers()) { @@ -114,6 +128,8 @@ public partial class AvaloniaPropertyAnalyzer var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; + var semanticModels = new ConcurrentDictionary(); + Parallel.ForEach(types, parallelOptions, type => { try @@ -133,38 +149,17 @@ public partial class AvaloniaPropertyAnalyzer foreach (var constructor in type.StaticConstructors) { - foreach (var syntaxRef in constructor.DeclaringSyntaxReferences) + foreach (var syntaxRef in constructor.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree))) { - var node = syntaxRef.GetSyntax(cancellationToken); - if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) - { - continue; - } - - var model = compilation.GetSemanticModel(node.SyntaxTree); + var (node, model) = GetNodeAndModel(syntaxRef); - foreach (var descendant in node.DescendantNodes()) + foreach (var descendant in node.DescendantNodes().Where(n => n.IsKind(SyntaxKind.SimpleAssignmentExpression))) { - switch (descendant.Kind()) - { - case SyntaxKind.SimpleAssignmentExpression: - var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!; - - var target = assignmentOperation.Target switch - { - IFieldReferenceOperation fieldRef => fieldRef.Field, - IPropertyReferenceOperation propertyRef => propertyRef.Property, - _ => default(ISymbol), - }; - - if (target == null) - { - break; - } + var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!; - RegisterAssignment(target, assignmentOperation.Value); - - break; + if (GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target) + { + RegisterAssignment(target, assignmentOperation.Value); } } } @@ -178,15 +173,10 @@ public partial class AvaloniaPropertyAnalyzer Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol => { - foreach (var syntaxRef in symbol.DeclaringSyntaxReferences) + foreach (var syntaxRef in symbol.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree))) { - var node = syntaxRef.GetSyntax(cancellationToken); - if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) - { - continue; - } + var (node, model) = GetNodeAndModel(syntaxRef); - var model = compilation.GetSemanticModel(node.SyntaxTree); var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType().FirstOrDefault(); if (operation == null) @@ -199,7 +189,7 @@ public partial class AvaloniaPropertyAnalyzer }); // we have recorded every Register and AddOwner call. Now follow assignment chains. - foreach (var root in fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray()) + Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), root => { var propertyDescription = propertyDescriptions[root]; var owner = propertyDescription.AssignedTo[root]; @@ -209,13 +199,13 @@ public partial class AvaloniaPropertyAnalyzer { var target = fieldInitializations[current]; - propertyDescription.AddAssignment(target, owner); // This loop handles simple assignment operations, so do NOT change the owner + propertyDescription.AddAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type propertyDescriptions[target] = propertyDescription; fieldInitializations.TryGetValue(target, out current); } while (current != null); - } + }); var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>(); @@ -251,7 +241,7 @@ public partial class AvaloniaPropertyAnalyzer // convert our dictionaries to immutable form _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer.Default) - .ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.ToImmutableArray(), SymbolEqualityComparer.Default); + .ToImmutableDictionary(g => g.Key, g => g.Distinct().ToImmutableArray(), SymbolEqualityComparer.Default); _avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default); void RegisterAssignment(ISymbol target, IOperation value) @@ -269,8 +259,12 @@ public partial class AvaloniaPropertyAnalyzer break; } } + + (SyntaxNode, SemanticModel) GetNodeAndModel(SyntaxReference syntaxRef) => + (syntaxRef.GetSyntax(cancellationToken), semanticModels.GetOrAdd(syntaxRef.SyntaxTree, st => compilation.GetSemanticModel(st))); } + // This method handles registration of a new AvaloniaProperty, and calls to AddOwner. private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary propertyDescriptions) { try @@ -280,49 +274,38 @@ public partial class AvaloniaPropertyAnalyzer return; } - if (_avaloniaPropertyRegisterMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AvaloniaProperty.Register* methods + var originalMethod = invocation.TargetMethod.OriginalDefinition; + + if (_avaloniaPropertyRegisterMethods.Contains(originalMethod)) // This is a call to one of the AvaloniaProperty.Register* methods { - if (!invocation.TargetMethod.IsGenericMethod) + TypeReference ownerTypeRef; + + if (_ownerTypeParams.TryGetValue(originalMethod, out var ownerTypeParam)) { - return; + ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); } - - var typeParamLookup = invocation.TargetMethod.TypeParameters.Select((s, i) => (param: s, index: i)) - .ToDictionary(t => t.param.Name, t => - { - var argument = invocation.TargetMethod.TypeArguments[t.index]; - - var typeArgumentSyntax = invocation.Syntax; - if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them - { - try - { - typeArgumentSyntax = invocation.Syntax.DescendantNodes() - .First(n => n.IsKind(SyntaxKind.TypeArgumentList)) - .DescendantNodes().ElementAt(t.index); - } - catch - { - // ignore, this is unimportant - } - } - - return new TypeReference((INamedTypeSymbol)argument, typeArgumentSyntax.GetLocation()); - }); - - if (!typeParamLookup.TryGetValue("TOwner", out var ownerTypeRef) && // if it's NOT a generic parameter, try to work out the runtime value - invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is INamedTypeSymbol ownerTypeParam && - invocation.Arguments.FirstOrDefault(a => SymbolEquals(a.Parameter, ownerTypeParam)) is IArgumentOperation argument) + else if (invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is { } ownerParam) // try extracting the runtime argument { - switch (ResolveOperationSource(argument.Value)) + switch (ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value)) { - case ITypeOfOperation typeOf when typeOf.Type is INamedTypeSymbol type: + case ITypeOfOperation { Type: INamedTypeSymbol type } typeOf: ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); break; + default: + return; } } + else + { + return; + } - if (ownerTypeRef.Type == null || !typeParamLookup.TryGetValue("TValue", out var propertyValueTypeRef)) + TypeReference valueTypeRef; + if (_valueTypeParams.TryGetValue(originalMethod, out var valueTypeParam)) + { + valueTypeRef = TypeReference.FromInvocationTypeParameter(invocation, valueTypeParam); + } + else { return; } @@ -343,47 +326,69 @@ public partial class AvaloniaPropertyAnalyzer return; } - var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueTypeRef.Type)); + var inherits = false; + if (_inheritsParams.TryGetValue(originalMethod, out var inheritsParam) && + invocation.Arguments[inheritsParam.Ordinal].Value is ILiteralOperation literalOp && + literalOp.ConstantValue.Value is bool constValue) + { + inherits = constValue; + } + + TypeReference? hostTypeRef = null; + if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) + { + if (_hostTypeParams.TryGetValue(originalMethod, out var hostTypeParam)) + { + hostTypeRef = TypeReference.FromInvocationTypeParameter(invocation, hostTypeParam); + } + else + { + hostTypeRef = new(_avaloniaObjectType, Location.None); + } + } + + var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, valueTypeRef.Type)); description.Name = name; + description.HostType = hostTypeRef; + description.Inherits = inherits; description.AddAssignment(target, ownerTypeRef); description.AddOwner(ownerTypeRef); } else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods { - if (invocation.TargetMethod.TypeArguments[0] is not INamedTypeSymbol ownerType) + if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam) || + invocation.TargetMethod.TypeArguments[ownerTypeParam.Ordinal] is not INamedTypeSymbol) { return; } - var ownerTypeRef = new TypeReference(ownerType, invocation.TargetMethod.TypeArguments[0].Locations[0]); - - ISymbol sourceSymbol; - switch (invocation.Instance) + if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol) { - case IFieldReferenceOperation fieldReference: - sourceSymbol = fieldReference.Field; - break; - case IPropertyReferenceOperation propertyReference: - sourceSymbol = propertyReference.Property; - break; - default: - return; + return; } - var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); - - var description = propertyDescriptions.GetOrAdd(target, s => + var description = propertyDescriptions.GetOrAdd(sourceSymbol, s => { - string inferredName = target.Name; + string inferredName = s.Name; - var match = Regex.Match(target.Name, "(?.*)Property$"); + var match = Regex.Match(s.Name, "(?.*)Property$"); if (match.Success) { inferredName = match.Groups["name"].Value; } - return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType); + + var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); + + TypeReference? hostTypeRef = null; + if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) + { + hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration + } + + return new AvaloniaPropertyDescription(inferredName, propertyType, propertyValueType) { HostType = hostTypeRef }; }); + var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); description.AddAssignment(target, ownerTypeRef); description.AddOwner(ownerTypeRef); } @@ -458,7 +463,7 @@ public partial class AvaloniaPropertyAnalyzer try { - var (target, isValid) = operation.Target switch + var (target, isValid) = ResolveOperationSource(operation.Target) switch { IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), @@ -508,6 +513,58 @@ public partial class AvaloniaPropertyAnalyzer } } + /// + /// + private void AnalyzeMethodInvocation(OperationAnalysisContext context) + { + var invocation = (IInvocationOperation)context.Operation; + + var originalMethod = invocation.TargetMethod.OriginalDefinition; + + if (_allGetSetMethods.Contains(originalMethod)) + { + var avaloniaPropertyOperation = invocation.Arguments[0].Value; + + var propertyStorageSymbol = GetReferencedFieldOrProperty(ResolveOperationSource(avaloniaPropertyOperation)); + + if (propertyStorageSymbol == null || !_avaloniaPropertyDescriptions.TryGetValue(propertyStorageSymbol, out var propertyDescription)) + { + return; + } + + TypeReference ownerOrHostType; + if (SymbolEquals(propertyDescription.PropertyType.OriginalDefinition, _attachedPropertyType)) + { + ownerOrHostType = propertyDescription.HostType!.Value; + } + else if (!propertyDescription.AssignedTo.TryGetValue(propertyStorageSymbol, out ownerOrHostType)) + { + return; + } + + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + !DerivesFrom(context.ContainingSymbol.ContainingType, ownerOrHostType.Type)) + { + context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(), + GetReferencedFieldOrProperty(avaloniaPropertyOperation), context.ContainingSymbol.ContainingType)); + } + } + else if (!IsStaticConstructorOrInitializer() && _allAvaloniaPropertyMethods.Contains(originalMethod)) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(), + originalMethod.ToDisplayString(TypeQualifiedName))); + } + + bool IsStaticConstructorOrInitializer() => + context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.StaticConstructor } || + ResolveOperationTarget(invocation.Parent!) switch + { + IFieldInitializerOperation fieldInit when fieldInit.InitializedFields.All(f => f.IsStatic) => true, + IPropertyInitializerOperation propInit when propInit.InitializedProperties.All(p => p.IsStatic) => true, + _ => false, + }; + } + /// /// /// @@ -520,7 +577,7 @@ public partial class AvaloniaPropertyAnalyzer { if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) { - return; // does not refer to an AvaloniaProperty + return; // does not refer to an AvaloniaProperty } context.RegisterSymbolEndAction(context => diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index 0a59f43d6a..3b35e97810 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Runtime.Serialization; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -18,7 +19,8 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private const string TypeMismatchTag = "TypeMismatch"; private const string NameCollisionTag = "NameCollision"; - private const string AssociatedPropertyTag = "AssociatedProperty"; + private const string AssociatedClrPropertyTag = "AssociatedClrProperty"; + private const string InappropriateReadWriteTag = "InappropriateReadWrite"; private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new( "AVP0001", @@ -28,7 +30,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Info, isEnabledByDefault: false, "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( "AVP1000", @@ -39,9 +41,18 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, "AvaloniaProperty objects have static lifetimes and should be stored accordingly. Do not multiply construct the same property."); + private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new( + "AVP1001", + "Ensure that the same AvaloniaProperty cannot be registered twice", + "Unsafe registration: {0} should be called only in static constructors or static initializers", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "AvaloniaProperty objects have static lifetimes and should be created only once. To ensure this, only call Register or AddOwner in static constructors or static initializers."); + private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( "AVP1010", - "Avaloniaproperty objects should declare their owner to be the type in which they are stored", + "AvaloniaProperty objects should be owned be the type in which they are stored", "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", Category, DiagnosticSeverity.Warning, @@ -49,6 +60,16 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.", TypeMismatchTag); + private static readonly DiagnosticDescriptor UnexpectedPropertyAccess = new( + "AVP1011", + "An AvaloniaObject should be the owner of each AvaloniaProperty it reads or writes on itself", + "Unexpected property use: {0} is neither owned by nor attached to {1}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is possible to use any AvaloniaProperty with any AvaloniaObject. However, each AvaloniaProperty an object uses on itself should be either owned by that object, or attached to that object.", + InappropriateReadWriteTag); + private static readonly DiagnosticDescriptor DuplicatePropertyName = new( "AVP1020", "AvaloniaProperty names should be unique within each class", @@ -87,7 +108,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true, "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, create a Coerce method or a PropertyChanged subscriber.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor MissingAccessor = new( "AVP1031", @@ -97,7 +118,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true, "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor InconsistentAccessibility = new( "AVP1032", @@ -107,7 +128,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true, "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different acessibility from its associated AvaloniaProperty is ineffective.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( "AVP1040", @@ -117,12 +138,18 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true, "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.", - TypeMismatchTag, AssociatedPropertyTag); + TypeMismatchTag, AssociatedClrPropertyTag); + + private static readonly SymbolDisplayFormat TypeQualifiedName = new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( AssociatedAvaloniaProperty, InappropriatePropertyAssignment, + InappropriatePropertyRegistration, OwnerDoesNotMatchOuterType, + UnexpectedPropertyAccess, DuplicatePropertyName, AmbiguousPropertyName, PropertyNameMismatch, @@ -193,6 +220,31 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer } } + private static IOperation ResolveOperationTarget(IOperation operation) + { + while (true) + { + switch (operation) + { + case IConversionOperation conversion: + operation = conversion.Parent!; + break; + case ISimpleAssignmentOperation assignment: + operation = assignment.Target; + break; + default: + return operation; + } + } + } + + private static ISymbol? GetReferencedFieldOrProperty(IOperation? operation) => operation == null ? null : ResolveOperationSource(operation) switch + { + IFieldReferenceOperation fieldRef => fieldRef.Field, + IPropertyReferenceOperation propertyRef => propertyRef.Property, + _ => null, + }; + private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; @@ -228,12 +280,22 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer /// public INamedTypeSymbol ValueType { get; } + /// + /// Gets whether the value of this property is inherited from the parent AvaloniaObject. + /// + public bool Inherits { get; set; } + /// /// Gets the type which registered the property, and all types which have added themselves as owners. /// public IReadOnlyCollection OwnerTypes { get; private set; } private ConcurrentBag? _ownerTypes = new(); + /// + /// Gets the type to which an AttachedProperty is attached, or null if the property is StyledProperty or DirectProperty. + /// + public TypeReference? HostType { get; set; } + /// /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. /// @@ -314,6 +376,28 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Type = type; Location = location; } + + public static TypeReference FromInvocationTypeParameter(IInvocationOperation invocation, ITypeParameterSymbol typeParameter) + { + var argument = invocation.TargetMethod.TypeArguments[typeParameter.Ordinal]; + + var typeArgumentSyntax = invocation.Syntax; + if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them + { + try + { + typeArgumentSyntax = invocation.Syntax.DescendantNodes() + .First(n => n.IsKind(SyntaxKind.TypeArgumentList)) + .DescendantNodes().ElementAt(typeParameter.Ordinal); + } + catch + { + // ignore, this is just a nicety + } + } + + return new TypeReference((INamedTypeSymbol)argument, typeArgumentSyntax.GetLocation()); + } } private class SymbolEqualityComparer : IEqualityComparer where T : ISymbol From 884c85bc1a270038fd1b940383d9f5ea3feaae48 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sat, 4 Feb 2023 16:32:16 +0100 Subject: [PATCH 28/60] Generalise INamedTypeSymbol to ITypeSymbol Cache ownerType method params --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 49 +++++++------------ .../AvaloniaPropertyAnalyzer.cs | 19 +++---- 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 179105a1f5..386d332c41 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -37,6 +37,7 @@ public partial class AvaloniaPropertyAnalyzer private readonly INamedTypeSymbol _attachedPropertyType; private readonly INamedTypeSymbol _directPropertyType; private readonly ImmutableArray _allAvaloniaPropertyTypes; + private readonly ImmutableDictionary _propertyValueTypeParams; private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; private readonly ImmutableHashSet _allAvaloniaPropertyMethods; @@ -44,6 +45,7 @@ public partial class AvaloniaPropertyAnalyzer private readonly ImmutableDictionary _valueTypeParams; private readonly ImmutableDictionary _hostTypeParams; private readonly ImmutableDictionary _inheritsParams; + private readonly ImmutableDictionary _ownerParams; public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) { @@ -65,6 +67,10 @@ public partial class AvaloniaPropertyAnalyzer _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableArray(); + _propertyValueTypeParams = _allAvaloniaPropertyTypes.Select(p => p.TypeParameters.First(t => t.Name == "TValue")) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => p.ContainingType, SymbolEqualityComparer.Default); + _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); @@ -74,6 +80,7 @@ public partial class AvaloniaPropertyAnalyzer _valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters); _hostTypeParams = GetParamDictionary("THost", m => m.TypeParameters); _inheritsParams = GetParamDictionary("inherits", m => m.Parameters); + _ownerParams = GetParamDictionary("ownerType", m => m.Parameters); RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); @@ -216,7 +223,7 @@ public partial class AvaloniaPropertyAnalyzer { var nameMatches = propertyDescriptionsByName[propertyDescription.Name]; - foreach (var ownerType in propertyDescription.OwnerTypes.Select(o => o.Type).Distinct(SymbolEqualityComparer.Default)) + foreach (var ownerType in propertyDescription.OwnerTypes.Select(o => o.Type).Distinct(SymbolEqualityComparer.Default)) { if (ownerType.GetMembers(propertyDescription.Name).OfType().SingleOrDefault() is not { IsStatic: false } clrProperty) { @@ -284,16 +291,10 @@ public partial class AvaloniaPropertyAnalyzer { ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); } - else if (invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is { } ownerParam) // try extracting the runtime argument + else if (_ownerParams.TryGetValue(originalMethod, out var ownerParam) && // try extracting the runtime argument + ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value) is ITypeOfOperation { Type: ITypeSymbol type } typeOf) { - switch (ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value)) - { - case ITypeOfOperation { Type: INamedTypeSymbol type } typeOf: - ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); - break; - default: - return; - } + ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); } else { @@ -356,8 +357,7 @@ public partial class AvaloniaPropertyAnalyzer } else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods { - if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam) || - invocation.TargetMethod.TypeArguments[ownerTypeParam.Ordinal] is not INamedTypeSymbol) + if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam)) { return; } @@ -377,7 +377,12 @@ public partial class AvaloniaPropertyAnalyzer inferredName = match.Groups["name"].Value; } - var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); + if (!_propertyValueTypeParams.TryGetValue(propertyType.OriginalDefinition, out var propertyValueType)) + { + throw new InvalidOperationException($"{propertyType} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType})."); + } + + var valueType = propertyType.TypeArguments[propertyValueType.Ordinal]; TypeReference? hostTypeRef = null; if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType)) @@ -385,7 +390,7 @@ public partial class AvaloniaPropertyAnalyzer hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration } - return new AvaloniaPropertyDescription(inferredName, propertyType, propertyValueType) { HostType = hostTypeRef }; + return new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; }); var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); @@ -719,21 +724,5 @@ public partial class AvaloniaPropertyAnalyzer throw new AvaloniaAnalysisException($"Failed to process property accessor '{method}'.", ex); } } - - private INamedTypeSymbol AvaloniaPropertyType_GetValueType(INamedTypeSymbol type) - { - var compareType = type.IsGenericType ? type.ConstructUnboundGenericType().OriginalDefinition : type; - - if (SymbolEquals(compareType, _styledPropertyType) || SymbolEquals(compareType, _attachedPropertyType)) - { - return (INamedTypeSymbol)type.TypeArguments[0]; - } - else if (SymbolEquals(compareType, _directPropertyType)) - { - return (INamedTypeSymbol)type.TypeArguments[1]; - } - - throw new ArgumentException($"{type} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType}).", nameof(type)); - } } } diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index 3b35e97810..a6e38c6e66 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -172,14 +172,11 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer }); } - private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable()); + private static bool IsAvaloniaPropertyType(ITypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable()); - private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, IEnumerable propertyTypes) + private static bool IsAvaloniaPropertyType(ITypeSymbol type, IEnumerable propertyTypes) { - if (type.IsGenericType) - { - type = type.ConstructUnboundGenericType().OriginalDefinition; - } + type = type.OriginalDefinition; return propertyTypes.Any(t => SymbolEquals(type, t)); } @@ -278,7 +275,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer /// /// Gets the TValue type that the property stores. /// - public INamedTypeSymbol ValueType { get; } + public ITypeSymbol ValueType { get; } /// /// Gets whether the value of this property is inherited from the parent AvaloniaObject. @@ -308,7 +305,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer public IReadOnlyCollection PropertyWrappers { get; private set; } private ConcurrentBag? _propertyWrappers = new(); - public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, INamedTypeSymbol valueType) + public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, ITypeSymbol valueType) { Name = name; PropertyType = propertyType; @@ -368,10 +365,10 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private readonly struct TypeReference { - public INamedTypeSymbol Type { get; } + public ITypeSymbol Type { get; } public Location Location { get; } - public TypeReference(INamedTypeSymbol type, Location location) + public TypeReference(ITypeSymbol type, Location location) { Type = type; Location = location; @@ -396,7 +393,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer } } - return new TypeReference((INamedTypeSymbol)argument, typeArgumentSyntax.GetLocation()); + return new TypeReference(argument, typeArgumentSyntax.GetLocation()); } } From 28c1f6c32bd6745d361427f8f12a034c2b43e9ec Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sat, 4 Feb 2023 18:01:38 +0100 Subject: [PATCH 29/60] Added SettingOwnStyledPropertyValue --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 44 ++++++++++++++++--- .../AvaloniaPropertyAnalyzer.cs | 28 +++++++++--- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 386d332c41..fe00b95753 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -36,6 +36,8 @@ public partial class AvaloniaPropertyAnalyzer private readonly INamedTypeSymbol _styledPropertyType; private readonly INamedTypeSymbol _attachedPropertyType; private readonly INamedTypeSymbol _directPropertyType; + private readonly INamedTypeSymbol? _userControlType; + private readonly INamedTypeSymbol? _topLevelType; private readonly ImmutableArray _allAvaloniaPropertyTypes; private readonly ImmutableDictionary _propertyValueTypeParams; private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; @@ -62,6 +64,9 @@ public partial class AvaloniaPropertyAnalyzer _attachedPropertyType = GetTypeOrThrow("Avalonia.AttachedProperty`1"); _directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2"); + _userControlType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl"); + _topLevelType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.TopLevel"); + _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(methodComparer); @@ -86,7 +91,8 @@ public partial class AvaloniaPropertyAnalyzer context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); - context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzePropertyStorageAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzePropertyWrapperAssignment, OperationKind.SimpleAssignment); context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation); context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property); @@ -381,7 +387,7 @@ public partial class AvaloniaPropertyAnalyzer { throw new InvalidOperationException($"{propertyType} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType})."); } - + var valueType = propertyType.TypeArguments[propertyValueType.Ordinal]; TypeReference? hostTypeRef = null; @@ -462,7 +468,7 @@ public partial class AvaloniaPropertyAnalyzer } /// - private void AnalyzeAssignment(OperationAnalysisContext context) + private void AnalyzePropertyStorageAssignment(OperationAnalysisContext context) { var operation = (IAssignmentOperation)context.Operation; @@ -518,6 +524,34 @@ public partial class AvaloniaPropertyAnalyzer } } + /// + private void AnalyzePropertyWrapperAssignment(OperationAnalysisContext context) + { + var operation = (IAssignmentOperation)context.Operation; + + if (ResolveOperationSource(operation) is IParameterReferenceOperation && context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor }) + { + // We can consider `new MyType(myValue)` functionally equivalent to `new MyType() { Value = myValue }`. Both set a local value with an external parameter. + return; + } + + if (ResolveOperationTarget(operation) is IPropertyReferenceOperation propertyRef && + propertyRef.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + _clrPropertyToAvaloniaProperties.TryGetValue(propertyRef.Property, out var propertyDescriptions) && + propertyDescriptions.Any(p => !SymbolEquals(p.PropertyType.OriginalDefinition, _directPropertyType))) + { + if (DerivesFrom(propertyRef.Instance.Type, _userControlType) || DerivesFrom(propertyRef.Instance.Type, _topLevelType)) + { + // Special case: don't warn about local value assignment on a UserControl or TopLevel type. + // 1. We don't want to annoy new users, who start with these two types and don't understand binding priorities yet + // 2. Such controls either have no consumers, or are treated largely as a black box (i.e. no styles setting dynamic values) + return; + } + + context.ReportDiagnostic(Diagnostic.Create(SettingOwnStyledPropertyValue, operation.Syntax.GetLocation())); + } + } + /// /// private void AnalyzeMethodInvocation(OperationAnalysisContext context) @@ -530,7 +564,7 @@ public partial class AvaloniaPropertyAnalyzer { var avaloniaPropertyOperation = invocation.Arguments[0].Value; - var propertyStorageSymbol = GetReferencedFieldOrProperty(ResolveOperationSource(avaloniaPropertyOperation)); + var propertyStorageSymbol = GetReferencedFieldOrProperty(avaloniaPropertyOperation); if (propertyStorageSymbol == null || !_avaloniaPropertyDescriptions.TryGetValue(propertyStorageSymbol, out var propertyDescription)) { @@ -551,7 +585,7 @@ public partial class AvaloniaPropertyAnalyzer !DerivesFrom(context.ContainingSymbol.ContainingType, ownerOrHostType.Type)) { context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(), - GetReferencedFieldOrProperty(avaloniaPropertyOperation), context.ContainingSymbol.ContainingType)); + propertyStorageSymbol, context.ContainingSymbol.ContainingType)); } } else if (!IsStaticConstructorOrInitializer() && _allAvaloniaPropertyMethods.Contains(originalMethod)) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index a6e38c6e66..ee47db48b2 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -70,6 +70,17 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "It is possible to use any AvaloniaProperty with any AvaloniaObject. However, each AvaloniaProperty an object uses on itself should be either owned by that object, or attached to that object.", InappropriateReadWriteTag); + private static readonly DiagnosticDescriptor SettingOwnStyledPropertyValue = new( + "AVP1012", + "An AvaloniaObject not set a value for its own StyledProperty or AttachedProperty", + "Inappropriate assignment: An AvaloniaObject should never set its own StyledProperty or AttachedProperty values", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Setting a StyledProperty or AttachedProperty directly will overwrite values from lower-priority sources, such as styles and templates. This should only be done by control consumers," + + "not the control itself. Constructor parameters and assignments within UserControl or TopLevel types are exempt from this diagnostic.", + InappropriateReadWriteTag); + private static readonly DiagnosticDescriptor DuplicatePropertyName = new( "AVP1020", "AvaloniaProperty names should be unique within each class", @@ -150,6 +161,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer InappropriatePropertyRegistration, OwnerDoesNotMatchOuterType, UnexpectedPropertyAccess, + SettingOwnStyledPropertyValue, DuplicatePropertyName, AmbiguousPropertyName, PropertyNameMismatch, @@ -181,18 +193,20 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer return propertyTypes.Any(t => SymbolEquals(type, t)); } - private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol baseType) + private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol? baseType) { - while (type != null) + if (baseType != null) { - if (SymbolEquals(type, baseType)) + while (type != null) { - return true; - } + if (SymbolEquals(type, baseType)) + { + return true; + } - type = type.BaseType; + type = type.BaseType; + } } - return false; } From e372338e71848aa65617b4b64272f9cb356ede95 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sun, 5 Feb 2023 11:55:53 +0100 Subject: [PATCH 30/60] Added PropertyOwnedByGenericType Disable analyzer release tracking message; this analyzer won't be released separately any time soon --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 114 ++++++++++-------- .../AvaloniaPropertyAnalyzer.cs | 13 ++ 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index fe00b95753..b0acfa0db4 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -38,7 +38,7 @@ public partial class AvaloniaPropertyAnalyzer private readonly INamedTypeSymbol _directPropertyType; private readonly INamedTypeSymbol? _userControlType; private readonly INamedTypeSymbol? _topLevelType; - private readonly ImmutableArray _allAvaloniaPropertyTypes; + private readonly ImmutableHashSet _allAvaloniaPropertyTypes; private readonly ImmutableDictionary _propertyValueTypeParams; private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; @@ -70,7 +70,7 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers() .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(methodComparer); - _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableArray(); + _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableHashSet(SymbolEqualityComparer.Default); _propertyValueTypeParams = _allAvaloniaPropertyTypes.Select(p => p.TypeParameters.First(t => t.Name == "TValue")) .Where(p => p != null).Cast() @@ -79,7 +79,7 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); - _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(SymbolEqualityComparer.Default); + _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(methodComparer); _ownerTypeParams = GetParamDictionary("TOwner", m => m.TypeParameters); _valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters); @@ -95,7 +95,7 @@ public partial class AvaloniaPropertyAnalyzer context.RegisterOperationAction(AnalyzePropertyWrapperAssignment, OperationKind.SimpleAssignment); context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation); - context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property); + context.RegisterSymbolAction(AnalyzeWrapperCrlProperty, SymbolKind.Property); if (context.Compilation.Language == LanguageNames.CSharp) { @@ -513,7 +513,9 @@ public partial class AvaloniaPropertyAnalyzer { var ownerType = description.AssignedTo[assignmentSymbol]; - if (!IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType)) + if (ownerType.Type.TypeKind != TypeKind.Error && + !IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && + !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType)) { context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, ownerType.Location, ownerType.Type)); } @@ -554,6 +556,7 @@ public partial class AvaloniaPropertyAnalyzer /// /// + /// private void AnalyzeMethodInvocation(OperationAnalysisContext context) { var invocation = (IInvocationOperation)context.Operation; @@ -588,10 +591,19 @@ public partial class AvaloniaPropertyAnalyzer propertyStorageSymbol, context.ContainingSymbol.ContainingType)); } } - else if (!IsStaticConstructorOrInitializer() && _allAvaloniaPropertyMethods.Contains(originalMethod)) + else if (_allAvaloniaPropertyMethods.Contains(originalMethod)) { - context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(), - originalMethod.ToDisplayString(TypeQualifiedName))); + if (!IsStaticConstructorOrInitializer()) + { + context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(), + originalMethod.ToDisplayString(TypeQualifiedName))); + } + + if (_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var typeParam) && + invocation.TargetMethod.TypeArguments[typeParam.Ordinal] is INamedTypeSymbol { IsGenericType: true }) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location)); + } } bool IsStaticConstructorOrInitializer() => @@ -609,71 +621,69 @@ public partial class AvaloniaPropertyAnalyzer /// /// /// - private void StartPropertySymbolAnalysis(SymbolStartAnalysisContext context) + private void AnalyzeWrapperCrlProperty(SymbolAnalysisContext context) { var property = (IPropertySymbol)context.Symbol; + + if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) + { + return; // does not refer to an AvaloniaProperty + } + try { - if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) + if (candidateTargetProperties.Length > 1) { - return; // does not refer to an AvaloniaProperty + var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null).OrderBy(s => s!.Name); + context.ReportDiagnostic(Diagnostic.Create(AmbiguousPropertyName, property.Locations[0], candidateSymbols.SelectMany(s => s!.Locations), + property.ContainingType, property.Name, $"\n\t{string.Join("\n\t", candidateSymbols)}")); + return; } - context.RegisterSymbolEndAction(context => + var avaloniaPropertyDescription = candidateTargetProperties[0]; + var avaloniaPropertyStorage = avaloniaPropertyDescription.ClosestAssignmentFor(property.ContainingType); + + if (avaloniaPropertyStorage == null) { - if (candidateTargetProperties.Length > 1) - { - var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null); - context.ReportDiagnostic(Diagnostic.Create(AmbiguousPropertyName, property.Locations[0], candidateSymbols.SelectMany(s => s!.Locations), - property.ContainingType, property.Name, $"\n\t{string.Join("\n\t", candidateSymbols)}")); - return; - } + return; + } - var avaloniaPropertyDescription = candidateTargetProperties[0]; - var avaloniaPropertyStorage = avaloniaPropertyDescription.ClosestAssignmentFor(property.ContainingType); + context.ReportDiagnostic(Diagnostic.Create(AssociatedAvaloniaProperty, property.Locations[0], new[] { avaloniaPropertyStorage.Locations[0] }, + avaloniaPropertyDescription.PropertyType.Name, avaloniaPropertyStorage)); - if (avaloniaPropertyStorage == null) - { - return; - } - - context.ReportDiagnostic(Diagnostic.Create(AssociatedAvaloniaProperty, property.Locations[0], new[] { avaloniaPropertyStorage.Locations[0] }, - avaloniaPropertyDescription.PropertyType.Name, avaloniaPropertyStorage)); + if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType, includeNullability: true)) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyTypeMismatch, property.Locations[0], + avaloniaPropertyStorage, $"\t\n{string.Join("\t\n", avaloniaPropertyDescription.ValueType, property.Type)}")); + } - if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType, includeNullability: true)) - { - context.ReportDiagnostic(Diagnostic.Create(PropertyTypeMismatch, property.Locations[0], - avaloniaPropertyStorage, $"\t\n{string.Join("\t\n", avaloniaPropertyDescription.ValueType, property.Type)}")); - } + if (property.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility) + { + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, property.Locations[0], "property", avaloniaPropertyStorage)); + } - if (property.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility) - { - context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, property.Locations[0], "property", avaloniaPropertyStorage)); - } + VerifyAccessor(property.GetMethod, "readable", "get"); - VerifyAccessor(property.GetMethod, "readable", "get"); + if (!IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + { + VerifyAccessor(property.SetMethod, "writeable", "set"); + } - if (!IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType)) + void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) + { + if (method == null) { - VerifyAccessor(property.SetMethod, "writeable", "set"); + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); } - - void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) + else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) { - if (method == null) - { - context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); - } - else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) - { - context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); - } + context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage)); } - }); + } } catch (Exception ex) { - throw new AvaloniaAnalysisException($"Failed to analyse property '{property}'.", ex); + throw new AvaloniaAnalysisException($"Failed to analyse wrapper property '{property}'.", ex); } } diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index ee47db48b2..75027af3f8 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.Serialization; using Microsoft.CodeAnalysis; @@ -13,6 +14,7 @@ using Microsoft.CodeAnalysis.Operations; namespace Avalonia.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer { private const string Category = "AvaloniaProperty"; @@ -50,6 +52,16 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, "AvaloniaProperty objects have static lifetimes and should be created only once. To ensure this, only call Register or AddOwner in static constructors or static initializers."); + private static readonly DiagnosticDescriptor PropertyOwnedByGenericType = new( + "AVP1002", + "AvaloniaProperty objects should not have a generic type as their owner", + "Inadvisable registration: Generic types cannot be referenced from XAML. Create a non-generic type to be the owner of this AvaloniaProperty.", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "It is sometimes necessary to refer to an AvaloniaProperty in XAML by providing its class name. This cannot be achieved if property's owner is a generic type." + + " Additionally, a new AvaloniaProperty object will be generated each time a new version of the the generic owner type is constructed, which may be unexpected."); + private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( "AVP1010", "AvaloniaProperty objects should be owned be the type in which they are stored", @@ -159,6 +171,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer AssociatedAvaloniaProperty, InappropriatePropertyAssignment, InappropriatePropertyRegistration, + PropertyOwnedByGenericType, OwnerDoesNotMatchOuterType, UnexpectedPropertyAccess, SettingOwnStyledPropertyValue, From 0b7851fd92800cec3cf85564753f9419e05e0e07 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Sun, 5 Feb 2023 17:17:15 +0100 Subject: [PATCH 31/60] Added SuperfluousAddOwnerCall --- ...valoniaPropertyAnalyzer.CompileAnalyzer.cs | 85 ++++++++++++------- .../AvaloniaPropertyAnalyzer.cs | 21 ++++- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index b0acfa0db4..0a27602604 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -212,7 +212,7 @@ public partial class AvaloniaPropertyAnalyzer { var target = fieldInitializations[current]; - propertyDescription.AddAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type + propertyDescription.SetAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type propertyDescriptions[target] = propertyDescription; fieldInitializations.TryGetValue(target, out current); @@ -358,7 +358,7 @@ public partial class AvaloniaPropertyAnalyzer description.Name = name; description.HostType = hostTypeRef; description.Inherits = inherits; - description.AddAssignment(target, ownerTypeRef); + description.SetAssignment(target, ownerTypeRef); description.AddOwner(ownerTypeRef); } else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods @@ -373,7 +373,7 @@ public partial class AvaloniaPropertyAnalyzer return; } - var description = propertyDescriptions.GetOrAdd(sourceSymbol, s => + var description = propertyDescriptions[target] = propertyDescriptions.GetOrAdd(sourceSymbol, s => { string inferredName = s.Name; @@ -396,11 +396,16 @@ public partial class AvaloniaPropertyAnalyzer hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration } - return new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + + // assume that the property is owned by its containing type at the point of assignment, until we find its registration + result.SetAssignment(s, new(s.ContainingType, Location.None)); + + return result; }); var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam); - description.AddAssignment(target, ownerTypeRef); + description.SetAssignment(target, ownerTypeRef); description.AddOwner(ownerTypeRef); } } @@ -514,7 +519,7 @@ public partial class AvaloniaPropertyAnalyzer var ownerType = description.AssignedTo[assignmentSymbol]; if (ownerType.Type.TypeKind != TypeKind.Error && - !IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && + !IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType)) { context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, ownerType.Location, ownerType.Type)); @@ -565,30 +570,14 @@ public partial class AvaloniaPropertyAnalyzer if (_allGetSetMethods.Contains(originalMethod)) { - var avaloniaPropertyOperation = invocation.Arguments[0].Value; - - var propertyStorageSymbol = GetReferencedFieldOrProperty(avaloniaPropertyOperation); - - if (propertyStorageSymbol == null || !_avaloniaPropertyDescriptions.TryGetValue(propertyStorageSymbol, out var propertyDescription)) - { - return; - } - - TypeReference ownerOrHostType; - if (SymbolEquals(propertyDescription.PropertyType.OriginalDefinition, _attachedPropertyType)) - { - ownerOrHostType = propertyDescription.HostType!.Value; - } - else if (!propertyDescription.AssignedTo.TryGetValue(propertyStorageSymbol, out ownerOrHostType)) - { - return; - } - - if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && - !DerivesFrom(context.ContainingSymbol.ContainingType, ownerOrHostType.Type)) + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + GetReferencedProperty(invocation.Arguments[0]) is { } refProp && + refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && + !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && + !DerivesFrom(context.ContainingSymbol.ContainingType, refProp.description.HostType?.Type)) { context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(), - propertyStorageSymbol, context.ContainingSymbol.ContainingType)); + refProp.storageSymbol, context.ContainingSymbol.ContainingType)); } } else if (_allAvaloniaPropertyMethods.Contains(originalMethod)) @@ -599,10 +588,32 @@ public partial class AvaloniaPropertyAnalyzer originalMethod.ToDisplayString(TypeQualifiedName))); } - if (_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var typeParam) && - invocation.TargetMethod.TypeArguments[typeParam.Ordinal] is INamedTypeSymbol { IsGenericType: true }) + if (_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var typeParam) && + invocation.TargetMethod.TypeArguments[typeParam.Ordinal] is { } newOwnerType) { - context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location)); + if (newOwnerType is INamedTypeSymbol { IsGenericType: true }) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location)); + } + + if (_avaloniaPropertyAddOwnerMethods.Contains(originalMethod) && GetReferencedProperty(invocation.Instance!) is { } refProp) + { + var ownerMatches = refProp.description.AssignedTo.Where(kvp => !SymbolEquals(kvp.Key, context.ContainingSymbol) && DerivesFrom(newOwnerType, kvp.Value.Type)).ToArray(); + + if (ownerMatches.Any()) + { + var ownerMatchesExceptBaseTypes = ownerMatches.Where(m => !DerivesFrom(context.ContainingSymbol.ContainingType, m.Key.ContainingType, includeSelf: false)).ToArray(); + var routesMessage = ownerMatchesExceptBaseTypes.Length switch + { + 0 => "its base type", + 1 => ownerMatchesExceptBaseTypes.Single().Key.ToString(), + _ => $"{ownerMatches.Length} routes\n\t{string.Join("\n\t", ownerMatches.Select(kvp => kvp.Key))}" + }; + + context.ReportDiagnostic(Diagnostic.Create(SuperfluousAddOwnerCall, invocation.Syntax.GetLocation(), ownerMatches.Select(kvp => kvp.Value.Location), + newOwnerType, refProp.storageSymbol, routesMessage)); + } + } } } @@ -616,6 +627,18 @@ public partial class AvaloniaPropertyAnalyzer }; } + private (AvaloniaPropertyDescription description, ISymbol storageSymbol)? GetReferencedProperty(IOperation operation) + { + if (GetReferencedFieldOrProperty(operation) is { } storageSymbol && _avaloniaPropertyDescriptions.TryGetValue(storageSymbol, out var result)) + { + return (result, storageSymbol); + } + else + { + return null; + } + } + /// /// /// diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index 75027af3f8..f6f20992cc 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -93,6 +93,16 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer "not the control itself. Constructor parameters and assignments within UserControl or TopLevel types are exempt from this diagnostic.", InappropriateReadWriteTag); + private static readonly DiagnosticDescriptor SuperfluousAddOwnerCall = new( + "AVP1013", + "Do not add superfluous AvaloniaProperty owners", + "Superfluous owner: {0} is already an owner of {1} via {2}", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + "Ownership of an AvaloniaProperty is inherited along the type hierarchy. There is no need for a derived type to assert ownership over a base type's properties. This diagnostic can be a symptom of an incorrect property owner elsewhere.", + InappropriateReadWriteTag); + private static readonly DiagnosticDescriptor DuplicatePropertyName = new( "AVP1020", "AvaloniaProperty names should be unique within each class", @@ -175,6 +185,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer OwnerDoesNotMatchOuterType, UnexpectedPropertyAccess, SettingOwnStyledPropertyValue, + SuperfluousAddOwnerCall, DuplicatePropertyName, AmbiguousPropertyName, PropertyNameMismatch, @@ -206,10 +217,15 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer return propertyTypes.Any(t => SymbolEquals(type, t)); } - private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol? baseType) + private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol? baseType, bool includeSelf = true) { if (baseType != null) { + if (!includeSelf) + { + type = type?.BaseType; + } + while (type != null) { if (SymbolEquals(type, baseType)) @@ -266,6 +282,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer { IFieldReferenceOperation fieldRef => fieldRef.Field, IPropertyReferenceOperation propertyRef => propertyRef.Property, + IArgumentOperation argument => GetReferencedFieldOrProperty(argument.Value), _ => null, }; @@ -349,7 +366,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer public void AddPropertyWrapper(IPropertySymbol property) => (_propertyWrappers ?? throw new InvalidOperationException(SealedError)).Add(property); - public void AddAssignment(ISymbol assignmentTarget, TypeReference ownerType) => (_assignedTo ?? throw new InvalidOperationException(SealedError)).TryAdd(assignmentTarget, ownerType); + public void SetAssignment(ISymbol assignmentTarget, TypeReference ownerType) => (_assignedTo ?? throw new InvalidOperationException(SealedError))[assignmentTarget] = ownerType; public AvaloniaPropertyDescription Seal() { From 1269b19bfceeb82cbdc7cd15d1e35d4def893355 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Mon, 6 Feb 2023 19:18:23 +0100 Subject: [PATCH 32/60] Fix spelling errors, improved grammar --- .../AvaloniaPropertyAnalyzer.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index f6f20992cc..d065e136c8 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -26,7 +26,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new( "AVP0001", - "Identify the AvaloniaProperty associated with a CLR property", + "Identification of the AvaloniaProperty associated with a CLR property", "Associated AvaloniaProperty: {0} {1}", Category, DiagnosticSeverity.Info, @@ -36,7 +36,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( "AVP1000", - "Store AvaloniaProperty objects appropriately", + "AvaloniaProperty objects should be stored appropriately", "Incorrect AvaloniaProperty storage: {0} should be static and readonly", Category, DiagnosticSeverity.Warning, @@ -45,7 +45,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new( "AVP1001", - "Ensure that the same AvaloniaProperty cannot be registered twice", + "The same AvaloniaProperty should not be registered twice", "Unsafe registration: {0} should be called only in static constructors or static initializers", Category, DiagnosticSeverity.Warning, @@ -54,7 +54,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor PropertyOwnedByGenericType = new( "AVP1002", - "AvaloniaProperty objects should not have a generic type as their owner", + "AvaloniaProperty objects should not be owned by a generic type", "Inadvisable registration: Generic types cannot be referenced from XAML. Create a non-generic type to be the owner of this AvaloniaProperty.", Category, DiagnosticSeverity.Warning, @@ -64,7 +64,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( "AVP1010", - "AvaloniaProperty objects should be owned be the type in which they are stored", + "AvaloniaProperty objects should be owned by the type in which they are stored", "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", Category, DiagnosticSeverity.Warning, @@ -74,7 +74,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor UnexpectedPropertyAccess = new( "AVP1011", - "An AvaloniaObject should be the owner of each AvaloniaProperty it reads or writes on itself", + "An AvaloniaObject should own each AvaloniaProperty it reads or writes on itself", "Unexpected property use: {0} is neither owned by nor attached to {1}", Category, DiagnosticSeverity.Warning, @@ -84,8 +84,8 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor SettingOwnStyledPropertyValue = new( "AVP1012", - "An AvaloniaObject not set a value for its own StyledProperty or AttachedProperty", - "Inappropriate assignment: An AvaloniaObject should never set its own StyledProperty or AttachedProperty values", + "An AvaloniaObject should not set its own StyledProperty or AttachedProperty values", + "Inappropriate assignment: An AvaloniaObject should not set its own StyledProperty or AttachedProperty values", Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, @@ -95,7 +95,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor SuperfluousAddOwnerCall = new( "AVP1013", - "Do not add superfluous AvaloniaProperty owners", + "AvaloniaProperty owners should not be added superfluously", "Superfluous owner: {0} is already an owner of {1} via {2}", Category, DiagnosticSeverity.Warning, @@ -115,7 +115,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor AmbiguousPropertyName = new( "AVP1021", - "Ensure an umabiguous relationship between CLR properties and Avalonia properties within the same class", + "There should be an unambiguous relationship between the CLR properties and Avalonia properties of a class", "Name collision: {0} owns multiple Avalonia properties with the name '{1}' {2}", Category, DiagnosticSeverity.Warning, @@ -125,7 +125,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor PropertyNameMismatch = new( "AVP1022", - "Store each AvaloniaProperty object in a field or CLR property which reflects its name", + "An AvaloniaProperty object should be stored in a field or CLR property which reflects its name", "Bad name: An AvaloniaProperty named '{0}' is being assigned to {1}. These names do not relate.", Category, DiagnosticSeverity.Warning, @@ -135,7 +135,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor AccessorSideEffects = new( "AVP1030", - "Do not add side effects to StyledProperty accessors", + "StyledProperty accessors should not have side effects", "Side effects: '{0}' is an AvaloniaProperty which can be {1} without the use of this CLR property. This {2} accessor should do nothing except call {3}.", Category, DiagnosticSeverity.Warning, @@ -156,16 +156,16 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor InconsistentAccessibility = new( "AVP1032", "A CLR property and its accessors should be equally accessible as its associated AvaloniaProperty", - "Inconsistent accessibility: CLR {0} accessiblity does not match accessibility of {1}", + "Inconsistent accessibility: CLR {0} accessibility does not match accessibility of {1}", Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, - "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different acessibility from its associated AvaloniaProperty is ineffective.", + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different accessibility from its associated AvaloniaProperty is ineffective.", AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( "AVP1040", - "CLR property type should match associated AvaloniaProperty type", + "A CLR property type should match the associated AvaloniaProperty type", "Type mismatch: CLR property type differs from the value type of {0} {1}", Category, DiagnosticSeverity.Warning, From 75a47777c241b7623c05229dd1af913351dc18c8 Mon Sep 17 00:00:00 2001 From: Tom Edwards Date: Thu, 9 Feb 2023 13:23:13 +0100 Subject: [PATCH 33/60] Clean up diagnostic descriptions --- .../AvaloniaPropertyAnalyzer.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index d065e136c8..d1d9071d17 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -41,7 +41,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, - "AvaloniaProperty objects have static lifetimes and should be stored accordingly. Do not multiply construct the same property."); + "AvaloniaProperty objects have static lifetimes and should be stored accordingly."); private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new( "AVP1001", @@ -60,7 +60,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true, "It is sometimes necessary to refer to an AvaloniaProperty in XAML by providing its class name. This cannot be achieved if property's owner is a generic type." + - " Additionally, a new AvaloniaProperty object will be generated each time a new version of the the generic owner type is constructed, which may be unexpected."); + " Additionally, a new AvaloniaProperty object will be generated each time a new version of the generic owner type is constructed, which may be unexpected."); private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( "AVP1010", @@ -84,13 +84,15 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private static readonly DiagnosticDescriptor SettingOwnStyledPropertyValue = new( "AVP1012", - "An AvaloniaObject should not set its own StyledProperty or AttachedProperty values", - "Inappropriate assignment: An AvaloniaObject should not set its own StyledProperty or AttachedProperty values", + "An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values", + "Inappropriate assignment: An AvaloniaObject should use SetCurrentValue when setting its own StyledProperty or AttachedProperty values", Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, - "Setting a StyledProperty or AttachedProperty directly will overwrite values from lower-priority sources, such as styles and templates. This should only be done by control consumers," + - "not the control itself. Constructor parameters and assignments within UserControl or TopLevel types are exempt from this diagnostic.", + "The standard means of setting an AvaloniaProperty is to call the SetValue method (often via a CLR property setter). This will forcibly overwrite values from sources like styles and templates, " + + "which is something that should only be done by consumers of the control, not the control itself. Controls which want to set their own values should instead call the SetCurrentValue method, or " + + "refactor the property into a DirectProperty. An assignment is exempt from this diagnostic in two scenarios: when it is forwarding a constructor parameter, and when the target object is derived " + + "from UserControl or TopLevel.", InappropriateReadWriteTag); private static readonly DiagnosticDescriptor SuperfluousAddOwnerCall = new( @@ -120,7 +122,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, - "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its AddOwner method and provide new metadata.", + "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its OverrideMetadata or OverrideDefaultValue methods.", NameCollisionTag); private static readonly DiagnosticDescriptor PropertyNameMismatch = new( @@ -140,7 +142,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, - "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, create a Coerce method or a PropertyChanged subscriber.", + "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, consider: 1) adding a Coercion method, b) adding a static observer with AvaloniaProperty.Changed.AddClassHandler, and/or c) overriding the AvaloniaObject.OnPropertyChanged method.", AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor MissingAccessor = new( From e975468e9278b1f78fbb4a1afdb7334c301b41c3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 09:23:26 -0500 Subject: [PATCH 34/60] Remove unused CanOpenRead/CanOpenWrite --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 35 ++++++++----------- .../Platform/Storage/AndroidStorageItem.cs | 6 +--- .../Platform/Storage/FileIO/BclStorageFile.cs | 6 +--- .../Platform/Storage/IStorageFile.cs | 12 +------ .../Screenshots/FilePickerHandler.cs | 4 --- .../Storage/BrowserStorageProvider.cs | 2 -- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 6 +--- 7 files changed, 19 insertions(+), 52 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index f7b020678d..5f116f95b6 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -223,7 +223,7 @@ namespace ControlCatalog.Pages ShowOverwritePrompt = false }); - if (file is not null && file.CanOpenWrite) + if (file is not null) { // Sync disposal of StreamWriter is not supported on WASM #if NET6_0_OR_GREATER @@ -298,31 +298,26 @@ namespace ControlCatalog.Pages if (item is IStorageFile file) { resultText += @$" - CanOpenRead: {file.CanOpenRead} - CanOpenWrite: {file.CanOpenWrite} Content: "; - if (file.CanOpenRead) - { #if NET6_0_OR_GREATER - await using var stream = await file.OpenReadAsync(); + await using var stream = await file.OpenReadAsync(); #else - using var stream = await file.OpenReadAsync(); + using var stream = await file.OpenReadAsync(); #endif - using var reader = new System.IO.StreamReader(stream); + 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); - } + // 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); } } diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs index 9838bb06c8..9d6dd46d0e 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs @@ -177,11 +177,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false) { } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public Task OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false) ?? throw new InvalidOperationException("Failed to open content stream")); diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs index a4005d4f5f..5bf9ff9d9a 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs @@ -18,11 +18,7 @@ internal class BclStorageFile : IStorageBookmarkFile } public FileInfo FileInfo { get; } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public string Name => FileInfo.Name; public virtual bool CanBookmark => true; diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 4aa84e3ec4..2a0ce15279 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage; [NotClientImplementable] public interface IStorageFile : IStorageItem { - /// - /// Returns true, if file is readable. - /// - bool CanOpenRead { get; } - /// /// Opens a stream for read access. /// /// Task OpenReadAsync(); - - /// - /// Returns true, if file is writeable. - /// - bool CanOpenWrite { get; } - + /// /// Opens stream for writing to the file. /// diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs index a7d279741e..548d177643 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs @@ -68,10 +68,6 @@ namespace Avalonia.Diagnostics.Screenshots { return null; } - if (!result.CanOpenWrite) - { - throw new InvalidOperationException("Read-only file was selected."); - } return await result.OpenWriteAsync(); } diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 7e8e2e0990..7189ae4111 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -216,7 +216,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile { } - public bool CanOpenRead => true; public async Task OpenReadAsync() { try @@ -230,7 +229,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile } } - public bool CanOpenWrite => true; public async Task OpenWriteAsync() { try diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index ef0e2467dc..6fa65f1265 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -94,11 +94,7 @@ internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile public IOSStorageFile(NSUrl url) : base(url) { } - - public bool CanOpenRead => true; - - public bool CanOpenWrite => true; - + public Task OpenReadAsync() { return Task.FromResult(new IOSSecurityScopedStream(Url, FileAccess.Read)); From d1c329e819f958a64e4bd7c069ea5e3e3c5a04bb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 10:49:35 -0500 Subject: [PATCH 35/60] Pass file extensions to the browser backend in the file picker --- samples/ControlCatalog/Pages/DialogsPage.xaml.cs | 8 +++++++- .../Platform/Storage/FilePickerFileType.cs | 16 ++++++++++++++-- .../Avalonia.Browser/Interop/StorageHelper.cs | 2 +- .../Storage/BrowserStorageProvider.cs | 2 +- .../webapp/modules/storage/storageProvider.ts | 4 ++-- .../Avalonia.iOS/Storage/IOSStorageProvider.cs | 4 ++++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 5f116f95b6..587b34c9fe 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -82,7 +82,13 @@ namespace ControlCatalog.Pages return new List { FilePickerFileTypes.All, - FilePickerFileTypes.TextPlain + FilePickerFileTypes.TextPlain, + new("Binary Log") + { + Patterns = new[] { "*.binlog", "*.buildlog" }, + MimeTypes = new[] { "application/binlog", "application/buildlog" }, + AppleUniformTypeIdentifiers = new []{ "public.data" } + } }; } diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs index f9c7f9685d..7b0446e224 100644 --- a/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs +++ b/src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.IO; +using System.Linq; namespace Avalonia.Platform.Storage; @@ -21,7 +23,7 @@ public sealed class FilePickerFileType /// List of extensions in GLOB format. I.e. "*.png" or "*.*". /// /// - /// Used on Windows and Linux systems. + /// Used on Windows, Linux and Browser platforms. /// public IReadOnlyList? Patterns { get; set; } @@ -29,7 +31,7 @@ public sealed class FilePickerFileType /// List of extensions in MIME format. /// /// - /// Used on Android, Browser and Linux systems. + /// Used on Android, Linux and Browser platforms. /// public IReadOnlyList? MimeTypes { get; set; } @@ -41,4 +43,14 @@ public sealed class FilePickerFileType /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. /// public IReadOnlyList? AppleUniformTypeIdentifiers { get; set; } + + internal IReadOnlyList? TryGetExtensions() + { + // Converts random glob pattern to a simple extension name. + // GetExtension should be sufficient here. + // Only exception is "*.*proj" patterns that should be filtered as well. + return Patterns?.Select(Path.GetExtension) + .Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith(".")) + .ToArray()!; + } } diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index a4d7bcdb87..902dd03885 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -54,5 +54,5 @@ internal static partial class StorageHelper public static partial JSObject[] ItemsArray(JSObject item); [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] - public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); + 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 7189ae4111..08d7664719 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -147,7 +147,7 @@ internal class BrowserStorageProvider : IStorageProvider { var types = input? .Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) - .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) + .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray(), t.TryGetExtensions()?.ToArray())) .ToArray(); if (types?.Length == 0) { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index e621a1ed30..8259510ed2 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -62,9 +62,9 @@ export class StorageProvider { } } - public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { + public static createAcceptType(description: string, mimeTypes: string[], extensions: string[] | undefined): FilePickerAcceptType { const accept: Record = {}; - mimeTypes.forEach(a => { accept[a] = []; }); + mimeTypes.forEach(a => { accept[a] = extensions ?? []; }); return { description, accept }; } } diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index 376e988e74..ae2bf2a99d 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -43,6 +43,10 @@ internal class IOSStorageProvider : IStorageProvider { return f.AppleUniformTypeIdentifiers.Select(id => UTType.CreateFromIdentifier(id)); } + if (f.TryGetExtensions() is { } extensions && extensions.Any()) + { + return extensions.Select(id => UTType.CreateFromExtension(id.TrimStart('.'))); + } if (f.MimeTypes?.Any() == true) { return f.MimeTypes.Select(id => UTType.CreateFromMimeType(id)); From bd1928efacf84333c5f832cc88b0a1db2c120b1e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 14:51:33 -0500 Subject: [PATCH 36/60] Refactor Browser and Blazor startup code --- .../App.razor.cs | 11 +-- .../ControlCatalog.Browser.Blazor/Program.cs | 12 ++- samples/ControlCatalog.Browser/Program.cs | 20 ++++- samples/ControlCatalog.Browser/main.js | 3 - .../Platform/DefaultPlatformSettings.cs | 2 +- .../Avalonia.Browser.Blazor/AvaloniaView.cs | 14 +++- .../BlazorSingleViewLifetime.cs | 37 ++++----- src/Browser/Avalonia.Browser/AvaloniaView.cs | 5 +- .../Avalonia.Browser/BrowserAppBuilder.cs | 83 +++++++++++++++++++ .../BrowserPlatformSettings.cs | 40 ++++++--- .../BrowserSingleViewLifetime.cs | 51 +++++------- .../Interop/AvaloniaModule.cs | 4 +- .../webapp/modules/avalonia.ts | 18 +--- 13 files changed, 196 insertions(+), 104 deletions(-) create mode 100644 src/Browser/Avalonia.Browser/BrowserAppBuilder.cs diff --git a/samples/ControlCatalog.Browser.Blazor/App.razor.cs b/samples/ControlCatalog.Browser.Blazor/App.razor.cs index f38db2b055..c331625664 100644 --- a/samples/ControlCatalog.Browser.Blazor/App.razor.cs +++ b/samples/ControlCatalog.Browser.Blazor/App.razor.cs @@ -1,3 +1,5 @@ +using System; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser.Blazor; @@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor; public partial class App { - protected override void OnParametersSet() - { - AppBuilder.Configure() - .UseBlazor() - // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering - .SetupWithSingleViewLifetime(); - - base.OnParametersSet(); - } } diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index eb99ca518e..500055b405 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -1,6 +1,8 @@ using System; using System.Net.Http; using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Blazor; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using ControlCatalog.Browser.Blazor; @@ -9,9 +11,17 @@ public class Program { public static async Task Main(string[] args) { - await CreateHostBuilder(args).Build().RunAsync(); + var host = CreateHostBuilder(args).Build(); + await StartAvaloniaApp(); + await host.RunAsync(); } + public static async Task StartAvaloniaApp() + { + await AppBuilder.Configure() + .StartBlazorApp(); + } + public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 53b7c60a6f..4a4d8c7bb8 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -1,6 +1,8 @@ using System.Runtime.Versioning; +using System.Threading.Tasks; using Avalonia; using Avalonia.Browser; +using Avalonia.Controls; using ControlCatalog; using ControlCatalog.Browser; @@ -8,15 +10,27 @@ using ControlCatalog.Browser; internal partial class Program { - private static void Main(string[] args) + public static async Task Main(string[] args) { - BuildAvaloniaApp() + await BuildAvaloniaApp() .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }).SetupBrowserApp("out"); + }) + .StartBrowserApp("out"); } + // Example without a ISingleViewApplicationLifetime + // private static AvaloniaView _avaloniaView; + // public static async Task Main(string[] args) + // { + // await BuildAvaloniaApp() + // .SetupBrowserApp(); + // + // _avaloniaView = new AvaloniaView("out"); + // _avaloniaView.Content = new TextBlock { Text = "Hello world" }; + // } + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } diff --git a/samples/ControlCatalog.Browser/main.js b/samples/ControlCatalog.Browser/main.js index 87f8a4f943..9d90db8bd2 100644 --- a/samples/ControlCatalog.Browser/main.js +++ b/samples/ControlCatalog.Browser/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { registerAvaloniaModule } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -await registerAvaloniaModule(dotnetRuntime); - const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs index b5e7298b7e..08fcdb50aa 100644 --- a/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs +++ b/src/Avalonia.Base/Platform/DefaultPlatformSettings.cs @@ -37,7 +37,7 @@ namespace Avalonia.Platform }; } - public event EventHandler? ColorValuesChanged; + public virtual event EventHandler? ColorValuesChanged; protected void OnColorValuesChanged(PlatformColorValues colorValues) { diff --git a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs index 68efea31d6..1fc87fed2f 100644 --- a/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs @@ -30,12 +30,10 @@ public class AvaloniaView : ComponentBase builder.CloseElement(); } - protected override async Task OnInitializedAsync() + protected override void OnAfterRender(bool firstRender) { - if (OperatingSystem.IsBrowser()) + if (firstRender) { - await AvaloniaModule.ImportMain(); - _browserView = new Browser.AvaloniaView(_containerId); if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) { @@ -43,4 +41,12 @@ public class AvaloniaView : ComponentBase } } } + + protected override void OnInitialized() + { + if (!OperatingSystem.IsBrowser()) + { + throw new NotSupportedException("Avalonia doesn't support server-side Blazor"); + } + } } diff --git a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs index a7bb5a62df..1f62690aff 100644 --- a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs @@ -1,33 +1,28 @@ -using System.Runtime.Versioning; - +using System; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Browser.Interop; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Browser.Blazor; -public static class WebAppBuilder +public static class BlazorAppBuilder { - public static AppBuilder SetupWithSingleViewLifetime( - this AppBuilder builder) + /// + /// Configures blazor backend, loads avalonia javascript modules and creates a single view lifetime. + /// + /// Application builder. + /// Browser backend specific options. + public static async Task StartBlazorApp(this AppBuilder builder, BrowserPlatformOptions? options = null) { - return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); - } + options ??= new BrowserPlatformOptions(); + options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}"; - public static AppBuilder UseBlazor(this AppBuilder builder) - { - return builder - .UseBrowser() - .With(new BrowserPlatformOptions - { - FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}") - }); - } + builder = await BrowserAppBuilder.PreSetupBrowser(builder, options); - public static AppBuilder Configure() - where TApp : Application, new() - { - return AppBuilder.Configure() - .UseBlazor(); + builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 3f4aa0d0ba..775100d76b 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -20,7 +20,7 @@ using static System.Runtime.CompilerServices.RuntimeHelpers; namespace Avalonia.Browser { - public partial class AvaloniaView : ITextInputMethodImpl + public class AvaloniaView : ITextInputMethodImpl { private static readonly PooledList s_intermediatePointsPooledList = new(ClearMode.Never); private readonly BrowserTopLevelImpl _topLevelImpl; @@ -43,8 +43,9 @@ namespace Avalonia.Browser private bool _useGL; private ITextInputMethodClient? _client; + /// ID of the html element where avalonia content should be rendered. public AvaloniaView(string divId) - : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document.")) + : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id '{divId}' was not found in the html document.")) { } diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs new file mode 100644 index 0000000000..866c8ceca4 --- /dev/null +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Browser.Interop; + +namespace Avalonia.Browser; + +public class BrowserPlatformOptions +{ + /// + /// Defines paths where avalonia modules and service locator should be resolved. + /// If null, default path resolved depending on the backend (browser or blazor) is used. + /// + public Func? FrameworkAssetPathResolver { get; set; } +} + +public static class BrowserAppBuilder +{ + /// + /// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed parameter. + /// + /// Application builder. + /// ID of the html element where avalonia content should be rendered. + /// Browser backend specific options. + public static async Task StartBrowserApp(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) + { + if (mainDivId is null) + { + throw new ArgumentNullException(nameof(mainDivId)); + } + + builder = await PreSetupBrowser(builder, options); + + var lifetime = new BrowserSingleViewLifetime(); + builder + .AfterSetup(_ => + { + lifetime.View = new AvaloniaView(mainDivId); + }) + .SetupWithLifetime(lifetime); + } + + /// + /// Loads avalonia javascript modules and configures browser backend. + /// + /// Application builder. + /// Browser backend specific options. + /// + /// This method doesn't creates any avalonia views to be rendered. To do so create an object. + /// Alternatively, you can call method instead of . + /// + public static async Task SetupBrowserApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + { + builder = await PreSetupBrowser(builder, options); + + builder + .SetupWithoutStarting(); + } + + internal static async Task PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options) + { + options ??= new BrowserPlatformOptions(); + options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}"; + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(options); + + await AvaloniaModule.ImportMain(); + + if (builder.WindowingSubsystemInitializer is null) + { + builder = builder.UseBrowser(); + } + + return builder; + } + + public static AppBuilder UseBrowser( + this AppBuilder builder) + { + return builder + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) + .UseSkia(); + } +} diff --git a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs index 6084c5c7de..fa647d31b7 100644 --- a/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs +++ b/src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs @@ -1,4 +1,5 @@ -using Avalonia.Browser.Interop; +using System; +using Avalonia.Browser.Interop; using Avalonia.Platform; namespace Avalonia.Browser; @@ -7,25 +8,44 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings { private bool _isDarkMode; private bool _isHighContrast; - - public BrowserPlatformSettings() + private bool _isInitialized; + + public override event EventHandler? ColorValuesChanged { - var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + add { - _isDarkMode = isDarkMode; - _isHighContrast = isHighContrast; - OnColorValuesChanged(GetColorValues()); - }); - _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); - _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + EnsureBackend(); + base.ColorValuesChanged += value; + } + remove => base.ColorValuesChanged -= value; } public override PlatformColorValues GetColorValues() { + EnsureBackend(); + return base.GetColorValues() with { ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference }; } + + private void EnsureBackend() + { + if (!_isInitialized) + { + // WASM module has async nature of initialization. We can't native code right away during components registration. + _isInitialized = true; + + var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => + { + _isDarkMode = isDarkMode; + _isHighContrast = isHighContrast; + OnColorValuesChanged(GetColorValues()); + }); + _isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); + _isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); + } + } } diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index add69760ee..6fa79f6f54 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -1,47 +1,36 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using System.Runtime.Versioning; +using Avalonia.Browser; -namespace Avalonia.Browser; +namespace Avalonia; -public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; public Control? MainView { - get => View!.Content; - set => View!.Content = value; - } -} - -public class BrowserPlatformOptions -{ - public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); -} - -public static class WebAppBuilder -{ - public static AppBuilder SetupBrowserApp( - this AppBuilder builder, string mainDivId) - { - var lifetime = new BrowserSingleViewLifetime(); - - return builder - .UseBrowser() - .AfterSetup(b => - { - lifetime.View = new AvaloniaView(mainDivId); - }) - .SetupWithLifetime(lifetime); + get + { + EnsureView(); + return View.Content; + } + set + { + EnsureView(); + View.Content = value; + } } - public static AppBuilder UseBrowser( - this AppBuilder builder) + [MemberNotNull(nameof(View))] + private void EnsureView() { - return builder - .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia(); + if (View is null) + { + throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called."); + } } } diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs index b283fbaa56..f1936a8d97 100644 --- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs +++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs @@ -11,13 +11,13 @@ internal static partial class AvaloniaModule public static Task ImportMain() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); + return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js")); } public static Task ImportStorage() { var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); - return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); + return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js")); } [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index ab0c85eaa2..3fb4124c96 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -1,4 +1,3 @@ -import { RuntimeAPI } from "../types/dotnet"; import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; @@ -7,19 +6,6 @@ import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NavigationHelper } from "./avalonia/navigationHelper"; -async function registerAvaloniaModule(api: RuntimeAPI): Promise { - api.setModuleImports("avalonia", { - Caniuse, - Canvas, - InputHelper, - SizeWatcher, - DpiWatcher, - AvaloniaDOM, - StreamHelper, - NativeControlHost, - NavigationHelper - }); -} export { Caniuse, Canvas, @@ -29,7 +15,5 @@ export { AvaloniaDOM, StreamHelper, NativeControlHost, - NavigationHelper, - - registerAvaloniaModule + NavigationHelper }; From 49a6cbd5a6c6174c9745b2a22c75a239836d9412 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 17:31:29 -0500 Subject: [PATCH 37/60] Use polyfill for open file and directory dialogs --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 2 +- src/Browser/Avalonia.Browser/AvaloniaView.cs | 5 - .../Avalonia.Browser/Interop/StorageHelper.cs | 10 +- .../Storage/BrowserStorageProvider.cs | 18 ++- .../Avalonia.Browser/webapp/.eslintrc.json | 2 +- src/Browser/Avalonia.Browser/webapp/build.js | 2 +- .../webapp/modules/avalonia/caniuse.ts | 12 +- .../webapp/modules/avalonia/stream.ts | 3 +- .../webapp/modules/storage/storageItem.ts | 50 +++++--- .../webapp/modules/storage/storageProvider.ts | 25 ++-- .../Avalonia.Browser/webapp/package-lock.json | 117 ++++++++++++++++-- .../Avalonia.Browser/webapp/package.json | 4 +- .../Avalonia.Browser/webapp/tsconfig.json | 6 +- 13 files changed, 178 insertions(+), 78 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 587b34c9fe..61e10729cc 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -281,7 +281,7 @@ namespace ControlCatalog.Pages { ignoreTextChanged = true; lastSelectedDirectory = folder; - currentFolderBox.Text = folder?.Path.LocalPath; + currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString(); ignoreTextChanged = false; } async Task SetPickerResult(IReadOnlyCollection? items) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 775100d76b..3bb7260e55 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -381,12 +381,10 @@ namespace Avalonia.Browser { if (_useGL && (_jsGlInfo == null)) { - Console.WriteLine("nothing to render"); return; } if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) { - Console.WriteLine("nothing to render"); return; } @@ -459,7 +457,6 @@ namespace Avalonia.Browser void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { - Console.WriteLine("Set Client"); if (_client != null) { _client.SurroundingTextChanged -= SurroundingTextChanged; @@ -482,8 +479,6 @@ namespace Avalonia.Browser var surroundingText = _client.SurroundingText; InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); - - Console.WriteLine("Shown, focused and surrounded."); } else { diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs index 902dd03885..11beba6f2c 100644 --- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs @@ -5,14 +5,8 @@ namespace Avalonia.Browser.Interop; internal static partial class StorageHelper { - [JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)] - public static partial bool CanShowOpenFilePicker(); - - [JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)] - public static partial bool CanShowSaveFilePicker(); - - [JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)] - public static partial bool CanShowDirectoryPicker(); + [JSImport("Caniuse.hasNativeFilePicker", AvaloniaModule.MainModuleName)] + public static partial bool HasNativeFilePicker(); [JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)] public static partial Task SelectFolderDialog(JSObject? startIn); diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 08d7664719..7497e82384 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -20,9 +20,9 @@ internal class BrowserStorageProvider : IStorageProvider private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); - public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); - public bool CanSave => StorageHelper.CanShowSaveFilePicker(); - public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); + public bool CanOpen => true; + public bool CanSave => StorageHelper.HasNativeFilePicker(); + public bool CanPickFolder => true; public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { @@ -186,10 +186,15 @@ internal abstract class JSStorageItem : IStorageBookmarkItem dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); } - public bool CanBookmark => true; + public bool CanBookmark => StorageHelper.HasNativeFilePicker(); public Task SaveBookmarkAsync() { + if (!CanBookmark) + { + return Task.FromResult(null); + } + return StorageHelper.SaveBookmark(FileHandle); } @@ -200,6 +205,11 @@ internal abstract class JSStorageItem : IStorageBookmarkItem public Task ReleaseBookmarkAsync() { + if (!CanBookmark) + { + return Task.CompletedTask; + } + return StorageHelper.DeleteBookmark(FileHandle); } diff --git a/src/Browser/Avalonia.Browser/webapp/.eslintrc.json b/src/Browser/Avalonia.Browser/webapp/.eslintrc.json index 4b7e24987f..f4fb8e37bf 100644 --- a/src/Browser/Avalonia.Browser/webapp/.eslintrc.json +++ b/src/Browser/Avalonia.Browser/webapp/.eslintrc.json @@ -43,5 +43,5 @@ } ] }, - "ignorePatterns": ["types/*"] + "ignorePatterns": ["types/*","node_modules/*"] } diff --git a/src/Browser/Avalonia.Browser/webapp/build.js b/src/Browser/Avalonia.Browser/webapp/build.js index 81f863cac7..c1cbc84709 100644 --- a/src/Browser/Avalonia.Browser/webapp/build.js +++ b/src/Browser/Avalonia.Browser/webapp/build.js @@ -7,7 +7,7 @@ require("esbuild").build({ bundle: true, minify: true, format: "esm", - target: "es2016", + target: "es2018", platform: "browser", sourcemap: "linked", loader: { ".ts": "ts" } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts index e019f92113..8fdc3a5c01 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts @@ -1,14 +1,6 @@ export class Caniuse { - public static canShowOpenFilePicker(): boolean { - return typeof globalThis.showOpenFilePicker !== "undefined"; - } - - public static canShowSaveFilePicker(): boolean { - return typeof globalThis.showSaveFilePicker !== "undefined"; - } - - public static canShowDirectoryPicker(): boolean { - return typeof globalThis.showDirectoryPicker !== "undefined"; + public static hasNativeFilePicker(): boolean { + return "showSaveFilePicker" in globalThis; } public static isMobile(): boolean { diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts index 1f2c181edc..7c7769ea36 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts @@ -1,3 +1,4 @@ +import FileSystemWritableFileStream from "native-file-system-adapter/types/src/FileSystemWritableFileStream"; import { IMemoryView } from "../../types/dotnet"; export class StreamHelper { @@ -17,7 +18,7 @@ export class StreamHelper { const array = new Uint8Array(span.byteLength); span.copyTo(array); - const data: WriteParams = { + const data = { type: "write", data: array }; diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts index c6e5254329..8f47e61100 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts @@ -1,8 +1,10 @@ import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; +import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFileStream } from "native-file-system-adapter"; +import { Caniuse } from "../avalonia"; export class StorageItem { constructor( - public handle?: FileSystemHandle, + public handle?: FileSystemFileHandle | FileSystemDirectoryHandle, private readonly bookmarkId?: string, public wellKnownType?: WellKnownDirectory ) { @@ -27,39 +29,44 @@ export class StorageItem { } public static async openRead(item: StorageItem): Promise { - if (!(item.handle instanceof FileSystemFileHandle)) { + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } await item.verityPermissions("read"); - const file = await item.handle.getFile(); + const file = await (item.handle as FileSystemFileHandle).getFile(); return file; } public static async openWrite(item: StorageItem): Promise { - if (!(item.handle instanceof FileSystemFileHandle)) { + if (!item.handle || item.kind !== "file") { throw new Error("StorageItem is not a file"); } await item.verityPermissions("readwrite"); - return await item.handle.createWritable({ keepExistingData: true }); + return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true }); } public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { - const file = item.handle instanceof FileSystemFileHandle && - await item.handle.getFile(); - - if (!file) { + // 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(); + + if (!file) { + return null; + } + + return { + Size: file.size, + LastModified: file.lastModified, + Type: file.type + }; + } catch { return null; } - - return { - Size: file.size, - LastModified: file.lastModified, - Type: file.type - }; } public static async getItems(item: StorageItem): Promise { @@ -74,11 +81,16 @@ export class StorageItem { return new StorageItems(items); } - private async verityPermissions(mode: FileSystemPermissionMode): Promise { + private async verityPermissions(mode: "read" | "readwrite"): Promise { if (!this.handle) { return; } + // If we are using polyfill, let it decide permissions by itself, we can't request anything in this case. + if (!Caniuse.hasNativeFilePicker()) { + return; + } + if (await this.handle.queryPermission({ mode }) === "granted") { return; } @@ -93,7 +105,9 @@ export class StorageItem { if (item.bookmarkId) { return item.bookmarkId; } - if (!item.handle) { + + // Bookmarks are not supported with polyfill. + if (!item.handle || !Caniuse.hasNativeFilePicker()) { return null; } @@ -107,7 +121,7 @@ export class StorageItem { } public static async deleteBookmark(item: StorageItem): Promise { - if (!item.bookmarkId) { + if (!item.bookmarkId || !Caniuse.hasNativeFilePicker()) { return; } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts index 8259510ed2..750c38b8ea 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts @@ -1,14 +1,12 @@ import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; import { StorageItem, StorageItems } from "./storageItem"; +import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter"; declare global { type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; - type StartInDirectory = WellKnownDirectory | FileSystemHandle; - interface OpenFilePickerOptions { - startIn?: StartInDirectory; - } - interface SaveFilePickerOptions { - startIn?: StartInDirectory; + interface FilePickerAcceptType { + description?: string | undefined; + accept: Record; } } @@ -16,39 +14,40 @@ export class StorageProvider { public static async selectFolderDialog( startIn: StorageItem | null): Promise { // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. - const options: DirectoryPickerOptions = { + const options = { startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined) }; - const handle = await window.showDirectoryPicker(options); + const handle = await showDirectoryPicker(options as any); return new StorageItem(handle); } public static async openFileDialog( startIn: StorageItem | null, multiple: boolean, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise { - const options: OpenFilePickerOptions = { + const options = { startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), multiple, excludeAcceptAllOption, types: (types ?? undefined) }; - const handles = await window.showOpenFilePicker(options); - return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); + const handles = await showOpenFilePicker(options); + return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle))); } public static async saveFileDialog( startIn: StorageItem | null, suggestedName: string | null, types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise { - const options: SaveFilePickerOptions = { + const options = { startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), suggestedName: (suggestedName ?? undefined), excludeAcceptAllOption, types: (types ?? undefined) }; - const handle = await window.showSaveFilePicker(options); + // Always prefer native save file picker, as polyfill solutions are not reliable. + const handle = await (globalThis as any).showSaveFilePicker(options); return new StorageItem(handle); } diff --git a/src/Browser/Avalonia.Browser/webapp/package-lock.json b/src/Browser/Avalonia.Browser/webapp/package-lock.json index 2d875e84db..12757fd7a0 100644 --- a/src/Browser/Avalonia.Browser/webapp/package-lock.json +++ b/src/Browser/Avalonia.Browser/webapp/package-lock.json @@ -5,9 +5,11 @@ "packages": { "": { "name": "avalonia.browser", + "dependencies": { + "native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027" + }, "devDependencies": { "@types/emscripten": "^1.39.6", - "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^5.38.1", "esbuild": "^0.15.7", "eslint": "^8.24.0", @@ -170,12 +172,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/wicg-file-system-access": { - "version": "2020.9.5", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", - "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", - "dev": true - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.38.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", @@ -1573,6 +1569,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "optional": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2289,6 +2308,27 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/native-file-system-adapter": { + "version": "3.0.0", + "resolved": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.8.0" + }, + "optionalDependencies": { + "fetch-blob": "^3.2.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2301,6 +2341,25 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "optional": true, + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -3196,6 +3255,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "optional": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3366,12 +3434,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "@types/wicg-file-system-access": { - "version": "2020.9.5", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", - "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", - "dev": true - }, "@typescript-eslint/eslint-plugin": { "version": "5.38.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", @@ -4275,6 +4337,16 @@ "reusify": "^1.0.4" } }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "optional": true, + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4796,6 +4868,13 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "native-file-system-adapter": { + "version": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027", + "from": "native-file-system-adapter@github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027", + "requires": { + "fetch-blob": "^3.2.0" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4808,6 +4887,12 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "optional": true + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5446,6 +5531,12 @@ "spdx-expression-parse": "^3.0.0" } }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "optional": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/Browser/Avalonia.Browser/webapp/package.json b/src/Browser/Avalonia.Browser/webapp/package.json index 05a3976ccc..20dd8d4e6a 100644 --- a/src/Browser/Avalonia.Browser/webapp/package.json +++ b/src/Browser/Avalonia.Browser/webapp/package.json @@ -8,7 +8,6 @@ }, "devDependencies": { "@types/emscripten": "^1.39.6", - "@types/wicg-file-system-access": "^2020.9.5", "@typescript-eslint/eslint-plugin": "^5.38.1", "esbuild": "^0.15.7", "eslint": "^8.24.0", @@ -18,5 +17,8 @@ "eslint-plugin-promise": "^6.0.1", "npm-run-all": "^4.1.5", "typescript": "^4.8.3" + }, + "dependencies": { + "native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027" } } diff --git a/src/Browser/Avalonia.Browser/webapp/tsconfig.json b/src/Browser/Avalonia.Browser/webapp/tsconfig.json index ad0e727150..1450ce4c57 100644 --- a/src/Browser/Avalonia.Browser/webapp/tsconfig.json +++ b/src/Browser/Avalonia.Browser/webapp/tsconfig.json @@ -1,14 +1,16 @@ { "compilerOptions": { - "target": "es2016", + "target": "es2018", "module": "es2020", "strict": true, "sourceMap": true, "noEmitOnError": true, + "moduleResolution": "node", + "skipLibCheck": true, "isolatedModules": true, // we need it for esbuild "lib": [ "dom", - "es2016", + "es2018", "esnext.asynciterable" ] }, From 696c5232e76f68011313466bf51c158ab00cee01 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 20:12:38 -0500 Subject: [PATCH 38/60] Rename StartBrowserApp-like methods to end with "Async" suffix --- samples/ControlCatalog.Browser.Blazor/Program.cs | 2 +- samples/ControlCatalog.Browser/Program.cs | 2 +- .../Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs | 2 +- src/Browser/Avalonia.Browser/BrowserAppBuilder.cs | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/ControlCatalog.Browser.Blazor/Program.cs b/samples/ControlCatalog.Browser.Blazor/Program.cs index 500055b405..e68e9b14d9 100644 --- a/samples/ControlCatalog.Browser.Blazor/Program.cs +++ b/samples/ControlCatalog.Browser.Blazor/Program.cs @@ -19,7 +19,7 @@ public class Program public static async Task StartAvaloniaApp() { await AppBuilder.Configure() - .StartBlazorApp(); + .StartBlazorAppAsync(); } public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) diff --git a/samples/ControlCatalog.Browser/Program.cs b/samples/ControlCatalog.Browser/Program.cs index 4a4d8c7bb8..e1a4500173 100644 --- a/samples/ControlCatalog.Browser/Program.cs +++ b/samples/ControlCatalog.Browser/Program.cs @@ -17,7 +17,7 @@ internal partial class Program { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); }) - .StartBrowserApp("out"); + .StartBrowserAppAsync("out"); } // Example without a ISingleViewApplicationLifetime diff --git a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs index 1f62690aff..7c5ee27c66 100644 --- a/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs @@ -15,7 +15,7 @@ public static class BlazorAppBuilder /// /// Application builder. /// Browser backend specific options. - public static async Task StartBlazorApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + public static async Task StartBlazorAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null) { options ??= new BrowserPlatformOptions(); options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}"; diff --git a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs index 866c8ceca4..32637b6d1b 100644 --- a/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs +++ b/src/Browser/Avalonia.Browser/BrowserAppBuilder.cs @@ -21,7 +21,7 @@ public static class BrowserAppBuilder /// Application builder. /// ID of the html element where avalonia content should be rendered. /// Browser backend specific options. - public static async Task StartBrowserApp(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) + public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null) { if (mainDivId is null) { @@ -46,9 +46,9 @@ public static class BrowserAppBuilder /// Browser backend specific options. /// /// This method doesn't creates any avalonia views to be rendered. To do so create an object. - /// Alternatively, you can call method instead of . + /// Alternatively, you can call method instead of . /// - public static async Task SetupBrowserApp(this AppBuilder builder, BrowserPlatformOptions? options = null) + public static async Task SetupBrowserAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null) { builder = await PreSetupBrowser(builder, options); From e6b8914b7d97b0c64f909e74832bf234c1c98032 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 14 Feb 2023 20:13:46 -0500 Subject: [PATCH 39/60] Rename TryGetFileFromPath-like method to end with Async suffix --- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 6 +-- .../Storage/FileIO/BclStorageProvider.cs | 6 +-- .../Platform/Storage/IStorageProvider.cs | 6 +-- .../Storage/StorageProviderExtensions.cs | 45 +++++++++---------- .../Platform/Dialogs/SystemDialogImpl.cs | 6 +-- .../Screenshots/FilePickerHandler.cs | 4 +- .../Internal/ManagedFileChooserViewModel.cs | 2 +- .../ManagedFileDialogExtensions.cs | 2 +- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 2 +- src/Avalonia.Native/SystemDialogs.cs | 6 +-- .../NativeDialogs/CompositeStorageProvider.cs | 12 ++--- .../NativeDialogs/GtkNativeFileDialogs.cs | 2 +- .../Avalonia.Win32/Win32StorageProvider.cs | 2 +- 13 files changed, 50 insertions(+), 51 deletions(-) diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 61e10729cc..e24860e3e1 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs @@ -40,7 +40,7 @@ namespace ControlCatalog.Pages if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum)) { - lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum); + lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum); } else { @@ -51,7 +51,7 @@ namespace ControlCatalog.Pages if (folderLink is not null) { - lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink); + lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink); } } }; @@ -148,7 +148,7 @@ namespace ControlCatalog.Pages } else { - SetFolder(await GetStorageProvider().TryGetFolderFromPath(result)); + SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result)); results.Items = new[] { result }; resultsVisible.IsVisible = true; } diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs index ee169d62a5..34409f5fda 100644 --- a/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs @@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider : Task.FromResult(null); } - public virtual Task TryGetFileFromPath(Uri filePath) + public virtual Task TryGetFileFromPathAsync(Uri filePath) { if (filePath.IsAbsoluteUri) { @@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider return Task.FromResult(null); } - public virtual Task TryGetFolderFromPath(Uri folderPath) + public virtual Task TryGetFolderFromPathAsync(Uri folderPath) { if (folderPath.IsAbsoluteUri) { @@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider return Task.FromResult(null); } - public virtual Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public virtual Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { // Note, this BCL API returns different values depending on the .NET version. // We should also document it. diff --git a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs index 6922151e02..9d3c961e51 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageProvider.cs @@ -66,7 +66,7 @@ public interface IStorageProvider /// It also might ask user for the permission, and throw an exception if it was denied. /// /// File or null if it doesn't exist. - Task TryGetFileFromPath(Uri filePath); + Task TryGetFileFromPathAsync(Uri filePath); /// /// Attempts to read folder from the file-system by its path. @@ -78,12 +78,12 @@ public interface IStorageProvider /// It also might ask user for the permission, and throw an exception if it was denied. /// /// Folder or null if it doesn't exist. - Task TryGetFolderFromPath(Uri folderPath); + Task TryGetFolderFromPathAsync(Uri folderPath); /// /// Attempts to read folder from the file-system by its path /// /// Well known folder identifier. /// Folder or null if it doesn't exist. - Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder); + Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder); } diff --git a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs index c7772d1196..6f8b945cd6 100644 --- a/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs +++ b/src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs @@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage; /// public static class StorageProviderExtensions { - /// - public static Task TryGetFileFromPath(this IStorageProvider provider, string filePath) + /// + public static Task TryGetFileFromPathAsync(this IStorageProvider provider, string filePath) { - return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath)); + return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath)); } - /// - public static Task TryGetFolderFromPath(this IStorageProvider provider, string folderPath) + /// + public static Task TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath) { - return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath)); + return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath)); } - internal static string? TryGetFullPath(this IStorageFolder folder) + /// + /// Gets the local file system path of the item as a string. + /// + /// Storage folder or file. + /// Full local path to the folder or file if possible, otherwise null. + /// + /// Android platform usually uses "content:" virtual file paths + /// and Browser platform has isolated access without full paths, + /// so on these platforms this method will return null. + /// + public static string? TryGetLocalPath(this IStorageItem item) { // We can avoid double escaping of the path by checking for BclStorageFolder. // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users. - if (folder is BclStorageFolder storageFolder) + if (item is BclStorageFolder storageFolder) { return storageFolder.DirectoryInfo.FullName; } - - if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) - { - return absolutePath.LocalPath; - } - - // android "content:", browser and ios relative links go here. - return null; - } - - internal static string? TryGetFullPath(this IStorageFile file) - { - if (file is BclStorageFile storageFolder) + if (item is BclStorageFile storageFile) { - return storageFolder.FileInfo.FullName; + return storageFile.FileInfo.FullName; } - if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) + if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) { return absolutePath.LocalPath; } + // android "content:", browser and ios relative links go here. return null; } } diff --git a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs index a8a266e378..20bfb440e3 100644 --- a/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.Platform var files = await filePicker.OpenFilePickerAsync(options); return files - .Select(file => file.TryGetFullPath() ?? file.Name) + .Select(file => file.TryGetLocalPath() ?? file.Name) .ToArray(); } else if (dialog is SaveFileDialog saveDialog) @@ -46,7 +46,7 @@ namespace Avalonia.Controls.Platform return null; } - var filePath = file.TryGetFullPath() ?? file.Name; + var filePath = file.TryGetLocalPath() ?? file.Name; return new[] { filePath }; } return null; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Platform var folders = await filePicker.OpenFolderPickerAsync(options); return folders - .Select(folder => folder.TryGetFullPath() ?? folder.Name) + .Select(folder => folder.TryGetLocalPath() ?? folder.Name) .FirstOrDefault(u => u is not null); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs index 548d177643..6ea46b6d54 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs @@ -54,8 +54,8 @@ namespace Avalonia.Diagnostics.Screenshots protected override async Task GetStream(Control control) { var storageProvider = GetTopLevel(control).StorageProvider; - var defaultFolder = await storageProvider.TryGetFolderFromPath(_screenshotRoot) - ?? await storageProvider.TryGetWellKnownFolder(WellKnownFolder.Pictures); + var defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot) + ?? await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures); var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { diff --git a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs index 46de460b1a..c929cb9ac9 100644 --- a/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs +++ b/src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs @@ -260,7 +260,7 @@ namespace Avalonia.Dialogs.Internal public void Navigate(IStorageFolder path, string initialSelectionName = null) { - var fullDirectoryPath = path?.TryGetFullPath() ?? Directory.GetCurrentDirectory(); + var fullDirectoryPath = path?.TryGetLocalPath() ?? Directory.GetCurrentDirectory(); Navigate(fullDirectoryPath, initialSelectionName); } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index cad938ac35..e9a75ab46a 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -51,7 +51,7 @@ namespace Avalonia.Dialogs var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions()); return files - .Select(file => file.TryGetFullPath() ?? file.Name) + .Select(file => file.TryGetLocalPath() ?? file.Name) .ToArray(); } } diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 6f647215be..905ce1f272 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop if (options.SuggestedFileName is { } currentName) chooserOptions.Add("current_name", currentName); - if (options.SuggestedStartLocation?.TryGetFullPath() is { } folderPath) + if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath)); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 60724507f3..1d23ad9997 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -33,7 +33,7 @@ namespace Avalonia.Native { using var events = new SystemDialogEvents(); - var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; + var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty; _native.OpenFileDialog((IAvnWindow)_window.Native, events, @@ -53,7 +53,7 @@ namespace Avalonia.Native { using var events = new SystemDialogEvents(); - var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; + var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty; _native.SaveFileDialog((IAvnWindow)_window.Native, events, @@ -72,7 +72,7 @@ namespace Avalonia.Native { using var events = new SystemDialogEvents(); - var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; + var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty; _native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory); diff --git a/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs index 07a11ff2ec..16416c7e56 100644 --- a/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs +++ b/src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs @@ -62,21 +62,21 @@ internal class CompositeStorageProvider : IStorageProvider return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false); } - public async Task TryGetFileFromPath(Uri filePath) + public async Task TryGetFileFromPathAsync(Uri filePath) { var provider = await EnsureStorageProvider().ConfigureAwait(false); - return await provider.TryGetFileFromPath(filePath).ConfigureAwait(false); + return await provider.TryGetFileFromPathAsync(filePath).ConfigureAwait(false); } - public async Task TryGetFolderFromPath(Uri folderPath) + public async Task TryGetFolderFromPathAsync(Uri folderPath) { var provider = await EnsureStorageProvider().ConfigureAwait(false); - return await provider.TryGetFolderFromPath(folderPath).ConfigureAwait(false); + return await provider.TryGetFolderFromPathAsync(folderPath).ConfigureAwait(false); } - public async Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var provider = await EnsureStorageProvider().ConfigureAwait(false); - return await provider.TryGetWellKnownFolder(wellKnownFolder).ConfigureAwait(false); + return await provider.TryGetWellKnownFolderAsync(wellKnownFolder).ConfigureAwait(false); } } diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index ceb32e52e9..16e1a1bce8 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -196,7 +196,7 @@ namespace Avalonia.X11.NativeDialogs gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); } - var folderLocalPath = initialFolder?.TryGetFullPath(); + var folderLocalPath = initialFolder?.TryGetLocalPath(); if (folderLocalPath is not null) { using var dir = new Utf8Buffer(folderLocalPath); diff --git a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs index 2fd49c8b09..86574f2fa3 100644 --- a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs +++ b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs @@ -131,7 +131,7 @@ namespace Avalonia.Win32 } } - if (folder?.TryGetFullPath() is { } folderPath) + if (folder?.TryGetLocalPath() is { } folderPath) { var riid = UnmanagedMethods.ShellIds.IShellItem; if (UnmanagedMethods.SHCreateItemFromParsingName(folderPath, IntPtr.Zero, ref riid, out var directoryShellItem) From 44b833a0af0645b3d99757e3d900834dfdeffd13 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Wed, 15 Feb 2023 15:20:05 +0200 Subject: [PATCH 40/60] Fix test --- samples/IntegrationTestApp/MainWindow.axaml | 4 ++-- tests/Avalonia.IntegrationTests.Appium/SliderTests.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 8fd33061df..72470873cb 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -151,8 +151,8 @@ - - + + diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index f6c50af59d..7fa5eb83ee 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -15,19 +15,19 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("Slider"); + var tab = tabs.FindElementByName("SliderTab"); tab.Click(); } [Fact] - public void Changes_Value_When_Moving_Slider() + public void Changes_Value_When_Clicking_Increase_Button() { - var slider = _session.FindElementByAccessibilityId("Slider2"); + var slider = _session.FindElementByAccessibilityId("Slider"); // slider.Text gets the Slider value Assert.True(double.Parse(slider.Text) == 30); - new Actions(_session).Click(slider).MoveByOffset(100, 0).Perform(); + new Actions(_session).Click(slider).Perform(); Assert.Equal(50, Math.Round(double.Parse(slider.Text))); } From 822f5d04aadf889eccb0ae12d16aa1f3b3708b05 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 15 Feb 2023 20:00:31 +0100 Subject: [PATCH 41/60] Fix Direct2D1 test runner hanging --- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 57379d1878..287db92b4d 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; -using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; @@ -55,15 +55,18 @@ namespace Avalonia.Direct2D1 return; } #if DEBUG - try + if (Debugger.IsAttached) { - Direct2D1Factory = new SharpDX.Direct2D1.Factory1( - SharpDX.Direct2D1.FactoryType.MultiThreaded, + try + { + Direct2D1Factory = new SharpDX.Direct2D1.Factory1( + SharpDX.Direct2D1.FactoryType.MultiThreaded, SharpDX.Direct2D1.DebugLevel.Error); - } - catch - { - // + } + catch + { + // ignore, retry below without the debug layer + } } #endif if (Direct2D1Factory == null) From 9a5b351b50866261a058a10a29fb054b7c4c5c17 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 16 Feb 2023 01:48:37 -0500 Subject: [PATCH 42/60] Fix build after global rename --- .../Platform/Storage/AndroidStorageProvider.cs | 6 +++--- .../Avalonia.Browser/Storage/BrowserStorageProvider.cs | 6 +++--- src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs index f611f50164..e35bde0acd 100644 --- a/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs +++ b/src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs @@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider return Task.FromResult(new AndroidStorageFolder(_activity, uri, false)); } - public async Task TryGetFileFromPath(Uri filePath) + public async Task TryGetFileFromPathAsync(Uri filePath) { if (filePath is null) { @@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider return new AndroidStorageFile(_activity, androidUri); } - public async Task TryGetFolderFromPath(Uri folderPath) + public async Task TryGetFolderFromPathAsync(Uri folderPath) { if (folderPath is null) { @@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider return new AndroidStorageFolder(_activity, androidUri, false); } - public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var dirCode = wellKnownFolder switch { diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs index 7497e82384..5b76d53a9d 100644 --- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs +++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs @@ -116,17 +116,17 @@ internal class BrowserStorageProvider : IStorageProvider return item is not null ? new JSStorageFolder(item) : null; } - public Task TryGetFileFromPath(Uri filePath) + public Task TryGetFileFromPathAsync(Uri filePath) { return Task.FromResult(null); } - public Task TryGetFolderFromPath(Uri folderPath) + public Task TryGetFolderFromPathAsync(Uri folderPath) { return Task.FromResult(null); } - public async Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public async Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { await _lazyModule.Value; var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs index ae2bf2a99d..9f27bfbbf5 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageProvider.cs @@ -104,19 +104,19 @@ internal class IOSStorageProvider : IStorageProvider ? new IOSStorageFolder(url) : null); } - public Task TryGetFileFromPath(Uri filePath) + public Task TryGetFileFromPathAsync(Uri filePath) { // TODO: research if it's possible, maybe with additional permissions. return Task.FromResult(null); } - public Task TryGetFolderFromPath(Uri folderPath) + public Task TryGetFolderFromPathAsync(Uri folderPath) { // TODO: research if it's possible, maybe with additional permissions. return Task.FromResult(null); } - public Task TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) + public Task TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder) { var directoryType = wellKnownFolder switch { From 95b6343f8f217f281f59127dff4928113d5bc94b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Feb 2023 13:02:20 +0600 Subject: [PATCH 43/60] Respect AdornerLayer.IsClipEnabled --- .../Server/ServerCompositionVisual.cs | 5 +- src/Avalonia.Base/composition-schema.xml | 1 + .../Primitives/AdornerLayer.cs | 5 +- .../Controls/AdornerTests.cs | 81 +++++++++++------- ...s_Properly_Clipped_Clip_False.expected.png | Bin 0 -> 694 bytes ...s_Properly_Clipped_Clip_True.expected.png} | Bin ...s_Properly_Clipped_Clip_False.expected.png | Bin 0 -> 694 bytes ...s_Properly_Clipped_Clip_True.expected.png} | Bin 8 files changed, 56 insertions(+), 36 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png rename tests/TestFiles/Direct2D1/Controls/Adorner/{Focus_Adorner_Is_Properly_Clipped.expected.png => Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png} (100%) create mode 100644 tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png rename tests/TestFiles/Skia/Controls/Adorner/{Focus_Adorner_Is_Properly_Clipped.expected.png => Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png} (100%) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e33dc999dc..98be861afa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -48,7 +48,8 @@ namespace Avalonia.Rendering.Composition.Server { canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + if (AdornerIsClipped) + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); @@ -74,7 +75,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); - if (AdornedVisual != null) + if (AdornedVisual != null && AdornerIsClipped) canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 36fd9fe709..31722974ee 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -26,6 +26,7 @@ + diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 79719912ea..611d57a980 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -279,8 +279,11 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { if (adorner.CompositionVisual != null) + { adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; - + adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner); + } + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) diff --git a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs index c0159aecff..b158bf798d 100644 --- a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs +++ b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -18,56 +19,70 @@ public class AdornerTests : TestBase { } - [Fact] - public async Task Focus_Adorner_Is_Properly_Clipped() + async Task CheckAdornedContent(Control content, Control adorned, Control adorner, int width = 200, int height = 200, + [CallerMemberName] string testName = "") { - Border adorned; var tree = new Decorator { Child = new VisualLayerManager { - Child = new Border - { - Background = Brushes.Red, - Padding = new Thickness(10, 50, 10,10), - Child = new Border() - { - Background = Brushes.White, - ClipToBounds = true, - Padding = new Thickness(0, -30, 0, 0), - Child = adorned = new Border - { - Background = Brushes.Green, - VerticalAlignment = VerticalAlignment.Top, - Height = 100, - Width = 50 - } - } - } + Child = content }, - Width = 200, - Height = 200 - }; - var adorner = new Border - { - BorderThickness = new Thickness(2), - BorderBrush = Brushes.Black + Width = width, + Height = height }; - + var size = new Size(tree.Width, tree.Height); tree.Measure(size); tree.Arrange(new Rect(size)); - - + adorned.AttachedToVisualTree += delegate { AdornerLayer.SetAdornedElement(adorner, adorned); AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner); }; + tree.Measure(size); tree.Arrange(new Rect(size)); - await RenderToFile(tree); - CompareImages(skipImmediate: true); + await RenderToFile(tree, testName: testName); + CompareImages(skipImmediate: true, testName: testName); + } + + [Theory, + InlineData(true), + InlineData(false) + ] + public async Task Focus_Adorner_Is_Properly_Clipped(bool clip) + { + Border adorned; + var content = new Border + { + Background = Brushes.Red, + Padding = new Thickness(10, 50, 10, 10), + Child = new Border() + { + Background = Brushes.White, + ClipToBounds = true, + Padding = new Thickness(0, -30, 0, 0), + Child = adorned = new Border + { + Background = Brushes.Green, + VerticalAlignment = VerticalAlignment.Top, + Height = 100, + Width = 50 + } + } + }; + var adorner = new Border + { + BorderThickness = new Thickness(2), + BorderBrush = Brushes.Black + }; + if (!clip) + AdornerLayer.SetIsClipEnabled(adorner, false); + await CheckAdornedContent(content, adorned, adorner, + testName: "Focus_Adorner_Is_Properly_Clipped_Clip_" + clip); } + } \ No newline at end of file diff --git a/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png b/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..4821c22c397099cdae149b5ed9cc367ab9844a66 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|Qg8r;B4q z#hkZyHf9|*5OD}x`Q-5D=dUhYT6t$y!XgXX^pB4oHtwjJ{Y)yE_d&S)ZuW+7H31H$ zMg<~?g4?zSrk=UWJm-iq%ki5E0yz`dW4^N0P3QgLW5&VK=paB5(egO|^SaghJgOJ{ z{JQJyp7*kk6TiP-zxj{l>HRkp1X!3H2R{J}SlM62vcKQXJ!@IsU(oeb=eR}1G&7Et z+wU0H9Xb2^|4{J=IJAMjc`L^hHHRkp1X!3H2R{J}SlM62vcKQXJ!@IsU(oeb=eR}1G&7Et z+wU0H9Xb2^|4{J=IJAMjc`L^hH Date: Thu, 16 Feb 2023 10:14:01 +0100 Subject: [PATCH 44/60] Update Measure and Layout if a child of RelativePanel updates it's position. --- .../RelativePanel.AttachedProperties.cs | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs index 18cf96ddca..d2b91def7d 100644 --- a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -1,37 +1,30 @@ using Avalonia.Layout; +using Avalonia.Threading; namespace Avalonia.Controls { public partial class RelativePanel { - private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) - { - if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent) - { - layoutableParent.InvalidateArrange(); - } - } static RelativePanel() { ClipToBoundsProperty.OverrideDefaultValue(true); - AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - RightOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AffectsParentArrange( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); + + AffectsParentMeasure( + AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty, + AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty, + AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty, + AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty, + AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty, + AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty); } /// From 8280b83e95d1f6803194a27bd3c601cf120b21de Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Feb 2023 15:22:20 +0600 Subject: [PATCH 45/60] Automatically dispatch PropertyChanged notification to the UI thread --- .../Data/Core/IndexerNodeBase.cs | 4 ++-- .../Plugins/InpcPropertyAccessorPlugin.cs | 4 ++-- src/Avalonia.Base/Utilities/WeakEvents.cs | 24 +++++++++++++++---- .../PropertyInfoAccessorFactory.cs | 4 ++-- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 9ec256225b..57e4fa4a8e 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -22,7 +22,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } ValueChanged(GetValue(target)); @@ -39,7 +39,7 @@ namespace Avalonia.Data.Core if (target is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 7c2caf02b4..e8e3e6d509 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } private object? GetReferenceTarget() @@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } } diff --git a/src/Avalonia.Base/Utilities/WeakEvents.cs b/src/Avalonia.Base/Utilities/WeakEvents.cs index 6da899bab2..2f62564e0e 100644 --- a/src/Avalonia.Base/Utilities/WeakEvents.cs +++ b/src/Avalonia.Base/Utilities/WeakEvents.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Specialized; using System.ComponentModel; using System.Windows.Input; +using Avalonia.Threading; namespace Avalonia.Utilities; @@ -20,15 +21,30 @@ public class WeakEvents }); /// - /// Represents PropertyChanged event from + /// Represents PropertyChanged event from with auto-dispatching to the UI thread /// public static readonly WeakEvent - PropertyChanged = WeakEvent.Register( + ThreadSafePropertyChanged = WeakEvent.Register( (s, h) => { - PropertyChangedEventHandler handler = (_, e) => h(s, e); + bool unsubscribed = false; + PropertyChangedEventHandler handler = (_, e) => + { + if (Dispatcher.UIThread.CheckAccess()) + h(s, e); + else + Dispatcher.UIThread.Post(() => + { + if (!unsubscribed) + h(s, e); + }); + }; s.PropertyChanged += handler; - return () => s.PropertyChanged -= handler; + return () => + { + unsubscribed = true; + s.PropertyChanged -= handler; + }; }); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs index abb166a92b..9e58f81b55 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -121,7 +121,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) { - WeakEvents.PropertyChanged.Unsubscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this); } } @@ -138,7 +138,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings private void SubscribeToChanges() { if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) - WeakEvents.PropertyChanged.Subscribe(inpc, this); + WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this); } } From 80dc4dd0b7bc03ce957e3f7e7d25cae201b18a5d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:42:32 +0100 Subject: [PATCH 46/60] fix: iOS Nullable --- src/iOS/Avalonia.iOS/PlatformSettings.cs | 5 +- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 8 +- src/iOS/Avalonia.iOS/TextInputResponder.cs | 81 +++++++++++++------ 3 files changed, 62 insertions(+), 32 deletions(-) diff --git a/src/iOS/Avalonia.iOS/PlatformSettings.cs b/src/iOS/Avalonia.iOS/PlatformSettings.cs index 0ae290468b..082966f5b2 100644 --- a/src/iOS/Avalonia.iOS/PlatformSettings.cs +++ b/src/iOS/Avalonia.iOS/PlatformSettings.cs @@ -1,8 +1,7 @@ +#nullable enable using System; -using System.Linq; using Avalonia.Media; using Avalonia.Platform; -using Foundation; using UIKit; namespace Avalonia.iOS; @@ -10,7 +9,7 @@ namespace Avalonia.iOS; // TODO: ideally should be created per view/activity. internal class PlatformSettings : DefaultPlatformSettings { - private PlatformColorValues _lastColorValues; + private PlatformColorValues? _lastColorValues; public override PlatformColorValues GetColorValues() { diff --git a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs index 6fa65f1265..27bd8faf64 100644 --- a/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs +++ b/src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -24,8 +23,11 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem using (var doc = new UIDocument(url)) { - _filePath = doc.FileUrl?.Path ?? url.FilePathUrl.Path; - Name = doc.LocalizedName ?? System.IO.Path.GetFileName(_filePath) ?? url.FilePathUrl.LastPathComponent; + _filePath = doc.FileUrl?.Path ?? url.FilePathUrl?.Path ?? string.Empty; + Name = doc.LocalizedName + ?? System.IO.Path.GetFileName(_filePath) + ?? url.FilePathUrl?.LastPathComponent + ?? string.Empty; } } diff --git a/src/iOS/Avalonia.iOS/TextInputResponder.cs b/src/iOS/Avalonia.iOS/TextInputResponder.cs index 8862f4e392..e444a90506 100644 --- a/src/iOS/Avalonia.iOS/TextInputResponder.cs +++ b/src/iOS/Avalonia.iOS/TextInputResponder.cs @@ -1,14 +1,13 @@ using System; using System.Runtime.InteropServices; -using System.Runtime.Versioning; using Avalonia.Controls.Presenters; -using Foundation; -using ObjCRuntime; -using Avalonia.Input.TextInput; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Logging; using CoreGraphics; +using Foundation; +using ObjCRuntime; using UIKit; // ReSharper disable InconsistentNaming // ReSharper disable StringLiteralTypo @@ -25,6 +24,9 @@ partial class AvaloniaView [Adopts("UIKeyInput")] partial class TextInputResponder : UIResponder, IUITextInput { + private static AvaloniaEmptyTextPosition? _emptyPosition; + private static AvaloniaEmptyTextPosition EmptyPosition => _emptyPosition ??= new(); + private class AvaloniaTextRange : UITextRange, INSCopying { private UITextPosition? _start; @@ -67,6 +69,15 @@ partial class AvaloniaView public NSObject Copy(NSZone? zone) => new AvaloniaTextPosition(Index); } + private class AvaloniaEmptyTextPosition : UITextPosition, INSCopying + { + public AvaloniaEmptyTextPosition() + { + + } + public NSObject Copy(NSZone? zone) => this; + } + public TextInputResponder(AvaloniaView view, ITextInputMethodClient client) { _view = view; @@ -93,7 +104,25 @@ partial class AvaloniaView public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString()); - public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode; + public override UITextInputMode TextInputMode + { + get + { + var mode = UITextInputMode.CurrentInputMode; + // Can be empty see https://developer.apple.com/documentation/uikit/uitextinputmode/1614522-activeinputmodes + if (mode is null && UITextInputMode.ActiveInputModes.Length > 0) + { + mode = UITextInputMode.ActiveInputModes[0]; + } + // See: https://stackoverflow.com/a/33337483/20894223 + if (mode is null) + { + using var tv = new UITextView(); + mode = tv.TextInputMode; + } + return mode; + } + } [DllImport("/usr/lib/libobjc.dylib")] private static extern void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg); @@ -105,8 +134,8 @@ partial class AvaloniaView private readonly AvaloniaView _view; private string? _markedText; - - + + private void SurroundingTextChanged(object? sender, EventArgs e) { Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "SurroundingTextChanged"); @@ -153,9 +182,9 @@ partial class AvaloniaView switch (ReturnKeyType) { case UIReturnKeyType.Done: - case UIReturnKeyType.Go: - case UIReturnKeyType.Send: - case UIReturnKeyType.Search: + case UIReturnKeyType.Go: + case UIReturnKeyType.Send: + case UIReturnKeyType.Search: ResignFirstResponder(); break; } @@ -164,7 +193,7 @@ partial class AvaloniaView TextInput(text); } - + void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back); bool IUIKeyInput.HasText => true; @@ -176,8 +205,8 @@ partial class AvaloniaView Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex); string result = ""; - if(string.IsNullOrEmpty(_markedText)) - result = s.Text[r.StartIndex .. r.EndIndex]; + if (string.IsNullOrEmpty(_markedText)) + result = s.Text[r.StartIndex..r.EndIndex]; else { var span = new CombinedSpan3(s.Text.AsSpan().Slice(0, s.CursorOffset), @@ -214,7 +243,7 @@ partial class AvaloniaView void IUITextInput.UnmarkText() { Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.UnmarkText"); - if(_markedText == null) + if (_markedText == null) return; var commitString = _markedText; _markedText = null; @@ -239,15 +268,15 @@ partial class AvaloniaView Logger.TryGet(LogEventLevel.Debug, ImeLog) ?.Log(null, "IUIKeyInput.GetPosition {start} {offset}", pos.Index, (int)offset); - var res = GetPositionCore(pos, offset); - Logger.TryGet(LogEventLevel.Debug, ImeLog) - ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); - return res!; + var res = GetPositionCore(pos, offset); + Logger.TryGet(LogEventLevel.Debug, ImeLog) + ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); + return res!; } private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition pos, nint offset) { - + var end = pos.Index + (int)offset; if (end < 0) return null!; @@ -261,14 +290,14 @@ partial class AvaloniaView { var pos = (AvaloniaTextPosition)fromPosition; Logger.TryGet(LogEventLevel.Debug, ImeLog) - ?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset); + ?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset); var res = GetPositionCore(pos, inDirection, offset); Logger.TryGet(LogEventLevel.Debug, ImeLog) ?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index)); return res!; } - + private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition fromPosition, UITextLayoutDirection inDirection, nint offset) { @@ -348,7 +377,7 @@ partial class AvaloniaView CGRect IUITextInput.GetFirstRectForRange(UITextRange range) { - + Logger.TryGet(LogEventLevel.Debug, ImeLog)? .Log(null, "IUITextInput:GetFirstRectForRange"); // TODO: Query from the input client @@ -377,11 +406,11 @@ partial class AvaloniaView if (presenter is { }) { var hitResult = presenter.TextLayout.HitTestPoint(new Point(point.X, point.Y)); - + return new AvaloniaTextPosition(hitResult.TextPosition); } - return null; + return EmptyPosition; } UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange) @@ -440,7 +469,7 @@ partial class AvaloniaView NSDictionary? IUITextInput.MarkedTextStyle { get => null; - set {} + set { } } UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument; @@ -478,7 +507,7 @@ partial class AvaloniaView var res = base.ResignFirstResponder(); if (res && ReferenceEquals(CurrentAvaloniaResponder, this)) { - + Logger.TryGet(LogEventLevel.Debug, "IOSIME") ?.Log(null, "Resigned first responder"); _client.SurroundingTextChanged -= SurroundingTextChanged; From 81f6f65c263a5e6fa27c9df3ef75c0515762818d Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 16 Feb 2023 11:34:37 +0100 Subject: [PATCH 47/60] Implement LayoutInformation. --- src/Avalonia.Base/Layout/LayoutInformation.cs | 27 +++++++++++++++++++ src/Avalonia.Base/Layout/Layoutable.cs | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Base/Layout/LayoutInformation.cs diff --git a/src/Avalonia.Base/Layout/LayoutInformation.cs b/src/Avalonia.Base/Layout/LayoutInformation.cs new file mode 100644 index 0000000000..9b821053a2 --- /dev/null +++ b/src/Avalonia.Base/Layout/LayoutInformation.cs @@ -0,0 +1,27 @@ +namespace Avalonia.Layout; + +/// +/// Provides access to layout information of a control. +/// +public static class LayoutInformation +{ + /// + /// Gets the available size constraint passed in the previous layout pass. + /// + /// The control. + /// Previous control measure constraint, if any. + public static Size? GetPreviousMeasureConstraint(Layoutable control) + { + return control.PreviousMeasure; + } + + /// + /// Gets the control bounds used in the previous layout arrange pass. + /// + /// The control. + /// Previous control arrange bounds, if any. + public static Rect? GetPreviousArrangeBounds(Layoutable control) + { + return control.PreviousArrange; + } +} diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index f14ad3058a..4a273b0291 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -326,12 +326,12 @@ namespace Avalonia.Layout /// /// Gets the available size passed in the previous layout pass, if any. /// - public Size? PreviousMeasure => _previousMeasure; + internal Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// - public Rect? PreviousArrange => _previousArrange; + internal Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary From 4c36520ed6a9b49893e2115bec42da540d44d1de Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 16 Feb 2023 17:35:02 +0100 Subject: [PATCH 48/60] Fix alignment helper not working and missing icon for clear button. --- .../Diagnostics/Controls/FilterTextBox.axaml | 2 ++ .../Diagnostics/ViewModels/ControlLayoutViewModel.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml index 3bfe511fbc..1b5f431f36 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml @@ -16,6 +16,8 @@