Browse Source

Use Tmds.DBus.Protocol

pull/9810/head
affederaffe 3 years ago
parent
commit
d7904b34cd
  1. 3
      .gitmodules
  2. 7
      Avalonia.Desktop.slnf
  3. 7
      Avalonia.sln
  4. 1
      NuGet.Config
  5. 176
      src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs
  6. 3
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  7. 643
      src/Avalonia.FreeDesktop/DBus.DBus.cs
  8. 32
      src/Avalonia.FreeDesktop/DBusFileChooser.cs
  9. 54
      src/Avalonia.FreeDesktop/DBusHelper.cs
  10. 58
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  11. 627
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs
  12. 69
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs
  13. 32
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs
  14. 47
      src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs
  15. 513
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs
  16. 52
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs
  17. 2
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs
  18. 44
      src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs
  19. 25
      src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs
  20. 463
      src/Avalonia.FreeDesktop/DBusMenu.DBus.cs
  21. 56
      src/Avalonia.FreeDesktop/DBusMenu.cs
  22. 432
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  23. 16
      src/Avalonia.FreeDesktop/DBusRequest.cs
  24. 107
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  25. 415
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  26. 937
      src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs
  27. 351
      src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs
  28. 6
      src/Avalonia.X11/X11Window.cs
  29. 1
      src/Linux/Tmds.DBus

3
.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

7
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",

7
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}

1
NuGet.Config

@ -6,5 +6,6 @@
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-eng" value="https://nuget.avaloniaui.net/repository/avalonia-devdeps/index.json" protocolVersion="3" />
<add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" />
<add key="tmds-dbus" value="https://www.myget.org/F/tmds/api/v3/index.json" />
</packageSources>
</configuration>

176
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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface,
MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<TProperties> changes, object? rs, object? hs) =>
((Action<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal,
MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)hs!).Invoke(ex, arg),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action<Exception?> handler,
bool emitOnCapturedContext)
{
var rule = new MatchRule
{
Type = MessageType.Signal,
Sender = sender,
Path = path,
Member = signal,
Interface = @interface
};
return Connection.AddMatchAsync<object>(rule, (Message message, object? state) => null!,
(Exception? ex, object v, object? rs, object? hs) => ((Action<Exception?>)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);
}
}
}

3
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -12,7 +12,8 @@
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<PackageReference Include="Tmds.DBus" Version="0.9.0" />
<ProjectReference Include="..\Linux\Tmds.DBus\src\Tmds.DBus.Protocol\Tmds.DBus.Protocol.csproj" />
<!--<PackageReference Include="Tmds.DBus.Protocol" Version="0.11.1-151-9f126c13809fceab8388328aff84b0d8e89b2aae" />-->
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />

643
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<string> 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<uint> 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<uint> 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<uint> 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<string, string> 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<bool> 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<string[]> 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<string[]> 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<string> 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<string[]> 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<uint> 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<uint> 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<byte[]> 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<byte[]> 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<string> 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<Dictionary<string, object>> 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<IDisposable> WatchNameOwnerChangedAsync(Action<Exception?, (string A0, string A1, string A2)> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "NameOwnerChanged", static (m, s) =>
ReadMessage_sss(m, (DBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchNameLostAsync(Action<Exception?, string> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "NameLost", static (m, s) =>
ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchNameAcquiredAsync(Action<Exception?, string> 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<string[]> GetFeaturesAsync() =>
Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Features"), static (m, s) =>
ReadMessage_v_as(m, (DBusObject)s!), this);
public Task<string[]> GetInterfacesAsync() =>
Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Interfaces"), static (m, s) =>
ReadMessage_v_as(m, (DBusObject)s!), this);
public Task<DBusProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<DBusProperties>> handler, bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, static (m, s) =>
ReadMessage(m, (DBusObject)s!), handler, emitOnCapturedContext);
static PropertyChanges<DBusProperties> ReadMessage(Message message, DBusObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new();
return new PropertyChanges<DBusProperties>(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? invalidated = null;
var headersEnd = reader.ReadArrayStart(DBusType.String);
while (reader.HasNext(headersEnd))
{
invalidated ??= new List<string>();
var property = reader.ReadString();
switch (property)
{
case "Features":
invalidated.Add("Features");
break;
case "Interfaces":
invalidated.Add("Interfaces");
break;
}
}
return invalidated?.ToArray() ?? Array.Empty<string>();
}
}
private static DBusProperties ReadProperties(ref Reader reader, List<string>? 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<string>();
changedList?.Add("Features");
break;
case "Interfaces":
reader.ReadSignature("as");
props.Interfaces = reader.ReadArray<string>();
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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface,
MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal,
MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action<Exception?> handler,
bool emitOnCapturedContext)
{
var rule = new MatchRule
{
Type = MessageType.Signal,
Sender = sender,
Path = path,
Member = signal,
Interface = @interface
};
return Connection.AddMatchAsync<object>(rule, static (_, _) => null!, static (ex, _, _, hs) =>
((Action<Exception?>)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<string>();
}
protected static byte[] ReadMessage_ay(Message message, DBusObject _)
{
var reader = message.GetBodyReader();
return reader.ReadArray<byte>();
}
protected static Dictionary<string, object> ReadMessage_aesv(Message message, DBusObject _)
{
var reader = message.GetBodyReader();
return reader.ReadDictionary<string, object>();
}
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<string>();
}
}
}

32
src/Avalonia.FreeDesktop/DBusFileChooser.cs

@ -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<ObjectPath> OpenFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFileAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<ObjectPath> SaveFilesAsync(string ParentWindow, string Title, IDictionary<string, object> Options);
Task<T> GetAsync<T>(string prop);
Task<FileChooserProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[Dictionary]
internal class FileChooserProperties
{
public uint Version { get; set; }
}
internal static class FileChooserExtensions
{
public static Task<uint> GetVersionAsync(this IFileChooser o) => o.GetAsync<uint>("version");
}
}

54
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
{
/// <summary>
/// This class uses synchronous execution at DBus connection establishment stage
/// then switches to using AvaloniaSynchronizationContext
/// </summary>
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)

58
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<bool> 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<bool> 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<X11InputMethodForwardedKey>? _onForward;
event Action<X11InputMethodForwardedKey> 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;

627
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<int> 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<IDisposable> WatchCommitStringAsync(Action<Exception?, string> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) =>
ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchCurrentIMAsync(Action<Exception?, (string Name, string UniqueName, string LangCode)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) =>
ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync(Action<Exception?, ((string, int)[] Str, int Cursorpos)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) =>
ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchForwardKeyAsync(Action<Exception?, (uint Keyval, uint State, int Type)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) =>
ReadMessage_uui(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchDeleteSurroundingTextAsync(Action<Exception?, (int Offset, uint Nchar)> 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<bool> 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<IDisposable> WatchCommitStringAsync(Action<Exception?, string> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) =>
ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchCurrentIMAsync(Action<Exception?, (string Name, string UniqueName, string LangCode)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) =>
ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdateFormattedPreeditAsync(Action<Exception?, ((string, int)[] Str, int Cursorpos)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) =>
ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchForwardKeyAsync(Action<Exception?, (uint Keyval, uint State, bool Type)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) =>
ReadMessage_uub(m, (FcitxObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchDeleteSurroundingTextAsync(Action<Exception?, (int Offset, uint Nchar)> 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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface,
MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal,
MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action<Exception?> handler,
bool emitOnCapturedContext)
{
var rule = new MatchRule
{
Type = MessageType.Signal,
Sender = sender,
Path = path,
Member = signal,
Interface = @interface
};
return Connection.AddMatchAsync<object>(rule, static (_, _) =>
null!, static (ex, _, _, hs) =>
((Action<Exception?>)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<byte>();
return (arg0, arg1);
}
}
}

69
src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs

@ -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<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time);
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? 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<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time);
Task<IDisposable?> WatchCommitStringAsync(Action<string> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception>? onError = null);
Task<IDisposable?> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.fcitx.Fcitx.InputMethod1")]
interface IFcitxInputMethod1 : IDBusObject
{
Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0);
}
}

32
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<bool> 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<IDisposable?> WatchCommitStringAsync(Action<string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? Task.FromResult(default(IDisposable?));
public ValueTask<IDisposable?> WatchCommitStringAsync(Action<Exception?, string> handler) =>
_old?.WatchCommitStringAsync(handler)
?? _modern?.WatchCommitStringAsync(handler)
?? new ValueTask<IDisposable?>(default(IDisposable?));
public Task<IDisposable?> 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<IDisposable?> WatchForwardKeyAsync(Action<Exception?, (uint keyval, uint state, int type)> handler) =>
_old?.WatchForwardKeyAsync(handler)
?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0)))
?? new ValueTask<IDisposable?>(default(IDisposable?));
public Task SetCapacityAsync(uint flags) =>
_old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask;

47
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<bool> Connect(string name)
{
var service = new FcitxService(Connection, name);
if (name == "org.fcitx.Fcitx")
{
var method = Connection.CreateProxy<IFcitxInputMethod>(name, "/inputmethod");
var method = service.CreateInputMethod("/inputmethod");
var resp = await method.CreateICv3Async(GetAppName(),
Process.GetCurrentProcess().Id);
var proxy = Connection.CreateProxy<IFcitxInputContext>(name,
"/inputcontext_" + resp.icid);
var proxy = service.CreateInputContext($"/inputcontext_{resp.Icid}");
_context = new FcitxICWrapper(proxy);
}
else
{
var method = Connection.CreateProxy<IFcitxInputMethod1>(name, "/inputmethod");
var method = service.CreateInputMethod1("/inputmethod");
var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) });
var proxy = Connection.CreateProxy<IFcitxInputContext1>(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<bool> 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);
}
}

513
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<ObjectPath> 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<bool> 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<object> 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<IDisposable> WatchCommitTextAsync(Action<Exception?, object> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "CommitText", static (m, s) =>
ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchForwardKeyEventAsync(Action<Exception?, (uint Keyval, uint Keycode, uint State)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKeyEvent", static (m, s) =>
ReadMessage_uuu(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdatePreeditTextAsync(Action<Exception?, (object Text, uint CursorPos, bool Visible)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditText", static (m, s) =>
ReadMessage_vub(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdatePreeditTextWithModeAsync(
Action<Exception?, (object Text, uint CursorPos, bool Visible, uint Mode)> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditTextWithMode", static (m, s) =>
ReadMessage_vubu(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchShowPreeditTextAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "ShowPreeditText", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchHidePreeditTextAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "HidePreeditText", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<Exception?, (object Text, bool Visible)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "UpdateAuxiliaryText", static (m, s) =>
ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchShowAuxiliaryTextAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "ShowAuxiliaryText", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchHideAuxiliaryTextAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "HideAuxiliaryText", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdateLookupTableAsync(Action<Exception?, (object Table, bool Visible)> handler,
bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "UpdateLookupTable", static (m, s) =>
ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchShowLookupTableAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "ShowLookupTable", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchHideLookupTableAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "HideLookupTable", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchPageUpLookupTableAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "PageUpLookupTable", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchPageDownLookupTableAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "PageDownLookupTable", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchCursorUpLookupTableAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "CursorUpLookupTable", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchCursorDownLookupTableAsync(Action<Exception?> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "CursorDownLookupTable", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchRegisterPropertiesAsync(Action<Exception?, object> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "RegisterProperties", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchUpdatePropertyAsync(Action<Exception?, object> 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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface,
MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal,
MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)hs!).Invoke(ex, arg),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action<Exception?> handler,
bool emitOnCapturedContext)
{
var rule = new MatchRule
{
Type = MessageType.Signal,
Sender = sender,
Path = path,
Member = signal,
Interface = @interface
};
return Connection.AddMatchAsync<object>(rule, static (_, _) => null!, static (ex, _, _, hs) => ((Action<Exception?>)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);
}
}
}

52
src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs

@ -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<bool> 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<object> GetEngineAsync();
Task DestroyAsync();
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos);
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception>? onError = null);
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception>? onError = null);
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception>? onError = null);
}
[DBusInterface("org.freedesktop.IBus.Portal")]
interface IIBusPortal : IDBusObject
{
Task<ObjectPath> CreateInputContextAsync(string Name);
}
}

2
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,

44
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<bool> Connect(string name)
{
var path =
await Connection.CreateProxy<IIBusPortal>(name, "/org/freedesktop/IBus")
.CreateInputContextAsync(GetAppName());
_context = Connection.CreateProxy<IIBusInputContext>(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
}
}
}

25
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<string, Func<Connection, IX11InputMethodFactory>> KnownMethods =
new Dictionary<string, Func<Connection, IX11InputMethodFactory>>
private static readonly Dictionary<string, Func<Connection, IX11InputMethodFactory>> KnownMethods = new()
{
["fcitx"] = conn =>
["fcitx"] = static conn =>
new DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)),
["ibus"] = conn =>
["ibus"] = static conn =>
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn))
};
static Func<Connection, IX11InputMethodFactory>? DetectInputMethod()
private static Func<Connection, IX11InputMethodFactory>? 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<IX11InputMethodFactory>().ToConstant(factory(conn));
return true;

463
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<string, object>, 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<string, object>)[]> 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<object> 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<int[]> 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<bool> 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<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<Exception?, ((int, Dictionary<string, object>)[] 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<IDisposable> WatchLayoutUpdatedAsync(Action<Exception?, (uint Revision, int Parent)> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", (m, s) => ReadMessage_ui(m, (DBusMenuObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchItemActivationRequestedAsync(Action<Exception?, (int Id, uint Timestamp)> 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<uint> GetVersionAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), (m, s) => ReadMessage_v_u(m, (DBusMenuObject)s!), this);
public Task<string> GetTextDirectionAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this);
public Task<string> GetStatusAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this);
public Task<string[]> GetIconThemePathAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), (m, s) => ReadMessage_v_as(m, (DBusMenuObject)s!), this);
public Task<DBusMenuProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<DBusMenuProperties>> handler, bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DBusMenuObject)s!), handler, emitOnCapturedContext);
static PropertyChanges<DBusMenuProperties> ReadMessage(Message message, DBusMenuObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new(), invalidated = new();
return new PropertyChanges<DBusMenuProperties>(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? 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<string>();
}
}
private static DBusMenuProperties ReadProperties(ref Reader reader, List<string>? 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<string>();
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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface, MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal, MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)hs!).Invoke(ex, arg),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action<Exception?> handler, bool emitOnCapturedContext)
{
var rule = new MatchRule
{
Type = MessageType.Signal,
Sender = sender,
Path = path,
Member = signal,
Interface = @interface
};
return Connection.AddMatchAsync<object>(rule, (message, state) => null!,
(ex, v, rs, hs) => ((Action<Exception?>)hs!).Invoke(ex), this, handler, emitOnCapturedContext);
}
protected static (uint, (int, Dictionary<string, object>, object[])) ReadMessage_uriaesvavz(Message message, DBusMenuObject _)
{
var reader = message.GetBodyReader();
var arg0 = reader.ReadUInt32();
var arg1 = reader.ReadStruct<int, Dictionary<string, object>, object[]>();
return (arg0, arg1);
}
protected static (int, Dictionary<string, object>)[] ReadMessage_ariaesvz(Message message, DBusMenuObject _)
{
var reader = message.GetBodyReader();
return reader.ReadArray<(int, Dictionary<string, object>)>();
}
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<int>();
}
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<int>();
var arg1 = reader.ReadArray<int>();
return (arg0, arg1);
}
protected static ((int, Dictionary<string, object>)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message, DBusMenuObject _)
{
var reader = message.GetBodyReader();
var arg0 = reader.ReadArray<(int, Dictionary<string, object>)>();
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<string>();
}
}
}

56
src/Avalonia.FreeDesktop/DBusMenu.cs

@ -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<object> GetAsync(string prop);
Task<DBusMenuProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[DBusInterface("com.canonical.dbusmenu")]
interface IDBusMenu : IFreeDesktopDBusProperties
{
Task<(uint revision, (int, KeyValuePair<string, object>[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames);
Task<(int, KeyValuePair<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames);
Task<object> GetPropertyAsync(int Id, string Name);
Task EventAsync(int Id, string EventId, object Data, uint Timestamp);
Task<int[]> EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events);
Task<bool> AboutToShowAsync(int Id);
Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids);
Task<IDisposable> WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? 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<IDisposable> WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action<Exception>? onError = null);
Task<IDisposable> WatchWindowUnregisteredAsync(Action<uint> handler, Action<Exception>? onError = null);
}
}

432
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<int, NativeMenuItemBase> _idsToItems = new();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new();
private readonly HashSet<NativeMenu> _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<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
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<IRegistrar>(
"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<object> IFreeDesktopDBusProperties.GetAsync(string prop)
{
if (prop == "Version")
return 2;
if (prop == "Status")
return "normal";
return 0;
}
async Task<DBusMenuProperties> 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 ?? "<null>";
return item.Header ?? "<null>";
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<IPlatformIconLoader>();
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<KeyValuePair<string, object>> _reusablePropertyList = new List<KeyValuePair<string, object>>();
KeyValuePair<string, object>[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names)
private Dictionary<string, object> 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<string, object>();
foreach (var n in names)
{
var v = GetProperty(i, n);
if (v != null)
_reusablePropertyList.Add(new KeyValuePair<string, object>(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<string, object>[], object[]) layout)> GetLayoutAsync(
int ParentId, int RecursionDepth, string[] PropertyNames)
private (int, Dictionary<string, object>, 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<string, object>[], 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<object>() : new object[menu.Items.Count];
if(menu != null)
var children = depth == 0 || menu is null ? Array.Empty<object>() : 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<string, object>[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames)
{
var arr = new (int, KeyValuePair<string, object>[])[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<object> 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<int[]> 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<int>());
}
public async Task<bool> AboutToShowAsync(int Id)
{
return false;
}
public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids)
{
return (Array.Empty<int>(), Array.Empty<int>());
}
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<string>();
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<int>();
var propertyNames = reader.ReadArray<string>();
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<int>());
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<int>(), Array.Empty<int>()));
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<string, object>)[] 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<PropertyChanges> PropertiesChanged { add { } remove { } }
break;
}
async Task<IDisposable> IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary<string, object>)[] updatedProps, (int, string[])[] removedProps)> handler, Action<Exception>? onError)
{
ItemsPropertiesUpdated += handler;
return Disposable.Create(() => ItemsPropertiesUpdated -= handler);
}
async Task<IDisposable> IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action<Exception>? onError)
{
LayoutUpdated += handler;
return Disposable.Create(() => LayoutUpdated -= handler);
return default;
}
async Task<IDisposable> IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action<Exception>? onError)
{
ItemActivationRequested+= handler;
return Disposable.Create(() => ItemActivationRequested -= handler);
}
public bool RunMethodHandlerSynchronously(Message message) => true;
async Task<IDisposable> IFreeDesktopDBusProperties.WatchPropertiesAsync(Action<PropertyChanges> 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
}
}
}

16
src/Avalonia.FreeDesktop/DBusRequest.cs

@ -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<IDisposable> WatchResponseAsync(Action<(uint response, IDictionary<string, object> results)> handler, Action<Exception>? onError = null);
}
}

107
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<IFileChooser?> s_fileChooser = new(() => DBusHelper.Connection?
.CreateProxy<IFileChooser>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"));
internal static async Task<IStorageProvider?> TryCreate(IPlatformHandle handle)
internal static async Task<IStorageProvider?> 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<string, object>();
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<IRequest>("org.freedesktop.portal.Request", objectPath);
var request = _desktopService.CreateRequest(objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task ?? Array.Empty<string>();
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<string>();
return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList();
}
public override async Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
@ -82,33 +74,33 @@ namespace Avalonia.FreeDesktop
var chooserOptions = new Dictionary<string, object>();
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<IRequest>("org.freedesktop.portal.Request", objectPath);
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = _desktopService.CreateRequest(objectPath);
var tsc = new TaskCompletionSource<string[]?>();
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<IReadOnlyList<IStorageFolder>> 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<IRequest>("org.freedesktop.portal.Request", objectPath);
var request = _desktopService.CreateRequest(objectPath);
var tsc = new TaskCompletionSource<string[]?>();
using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException);
var uris = await tsc.Task ?? Array.Empty<string>();
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<string>();
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<FilePickerFileType>? 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();

415
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<IStatusNotifierWatcher>(
"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
/// <remarks>
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
/// </remarks>
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<string>? 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<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError)
{
OnTitleChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler));
}
public Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError)
{
OnIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler));
}
public Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> 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<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError)
{
OnOverlayIconChanged += handler;
return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler));
}
public Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError)
{
OnTooltipChanged += handler;
return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler));
}
public Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError)
{
NewStatusAsync += handler;
return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
}
public Task<object?> GetAsync(string prop)
{
return Task.FromResult<object?>(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<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);
public Task SetAsync(string prop, object val) => Task.CompletedTask;
public Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> 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<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError);
Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError);
Task<object?> GetAsync(string prop);
Task<StatusNotifierItemProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> 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<string, object>
{
{ "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<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>())
};
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 });
}
}

937
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<string, object> 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<IDisposable> WatchActionInvokedAsync(Action<Exception?, (string Id, string Action, object[] Parameter)> 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<uint> GetVersionAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"),
(m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this);
public Task<NotificationProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<NotificationProperties>> handler,
bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler,
emitOnCapturedContext);
static PropertyChanges<NotificationProperties> ReadMessage(Message message, DesktopObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new();
return new PropertyChanges<NotificationProperties>(ReadProperties(ref reader, changed), changed.ToArray(),
ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? 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<string>();
}
}
private static NotificationProperties ReadProperties(ref Reader reader, List<string>? 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<ObjectPath> OpenUriAsync(string parentWindow, string uri, Dictionary<string, object> 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<ObjectPath> OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary<string, object> 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<ObjectPath> OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary<string, object> 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<uint> GetVersionAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"),
(m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this);
public Task<OpenURIProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<OpenURIProperties>> handler,
bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler,
emitOnCapturedContext);
static PropertyChanges<OpenURIProperties> ReadMessage(Message message, DesktopObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new();
return new PropertyChanges<OpenURIProperties>(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? 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<string>();
}
}
private static OpenURIProperties ReadProperties(ref Reader reader, List<string>? 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<string, object> 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<ObjectPath> PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary<string, object> 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<string> RequestInstallTokenAsync(string name, object iconV, Dictionary<string, object> 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<string, object> 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<string> 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<string, object> 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<uint> GetSupportedLauncherTypesAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"),
(m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this);
public Task<uint> GetVersionAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"),
(m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this);
public Task<DynamicLauncherProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<DynamicLauncherProperties>> handler,
bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler,
emitOnCapturedContext);
static PropertyChanges<DynamicLauncherProperties> ReadMessage(Message message, DesktopObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new();
return new PropertyChanges<DynamicLauncherProperties>(ReadProperties(ref reader, changed), changed.ToArray(),
ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? 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<string>();
}
}
private static DynamicLauncherProperties ReadProperties(ref Reader reader, List<string>? 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<ObjectPath> OpenFileAsync(string parentWindow, string title, Dictionary<string, object> 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<ObjectPath> SaveFileAsync(string parentWindow, string title, Dictionary<string, object> 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<ObjectPath> SaveFilesAsync(string parentWindow, string title, Dictionary<string, object> 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<uint> GetVersionAsync()
=> Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"),
(m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this);
public Task<FileChooserProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<FileChooserProperties>> handler,
bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler,
emitOnCapturedContext);
static PropertyChanges<FileChooserProperties> ReadMessage(Message message, DesktopObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new();
return new PropertyChanges<FileChooserProperties>(ReadProperties(ref reader, changed), changed.ToArray(),
ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? 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<string>();
}
}
private static FileChooserProperties ReadProperties(ref Reader reader, List<string>? 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<IDisposable> WatchResponseAsync(Action<Exception?, (uint response, IDictionary<string, object> 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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface,
MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal,
MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)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<object>();
return (arg0, arg1, arg2);
}
protected static (uint, Dictionary<string, object>) ReadMessage_uaesv(Message message, DesktopObject _)
{
var reader = message.GetBodyReader();
var arg0 = reader.ReadUInt32();
var arg1 = reader.ReadDictionary<string, object>();
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<TProperties>
{
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;
}
}

351
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<IDisposable> WatchStatusNotifierItemRegisteredAsync(Action<Exception?, string> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemRegistered", static (m, s) =>
ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchStatusNotifierItemUnregisteredAsync(Action<Exception?, string> handler, bool emitOnCapturedContext = true)
=> WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemUnregistered", static (m, s)
=> ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchStatusNotifierHostRegisteredAsync(Action<Exception?> handler, bool emitOnCapturedContext = true) =>
WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostRegistered", handler, emitOnCapturedContext);
public ValueTask<IDisposable> WatchStatusNotifierHostUnregisteredAsync(Action<Exception?> 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<string[]> GetRegisteredStatusNotifierItemsAsync() =>
Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "RegisteredStatusNotifierItems"), static (m, s)
=> ReadMessage_v_as(m, (StatusNotifierWatcherObject)s!), this);
public Task<bool> GetIsStatusNotifierHostRegisteredAsync() =>
Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IsStatusNotifierHostRegistered"), static (m, s) =>
ReadMessage_v_b(m, (StatusNotifierWatcherObject)s!), this);
public Task<int> GetProtocolVersionAsync() =>
Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "ProtocolVersion"), static (m, s)
=> ReadMessage_v_i(m, (StatusNotifierWatcherObject)s!), this);
public Task<StatusNotifierWatcherProperties> 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<IDisposable> WatchPropertiesChangedAsync(Action<Exception?, PropertyChanges<StatusNotifierWatcherProperties>> handler, bool emitOnCapturedContext = true)
{
return base.WatchPropertiesChangedAsync(Interface, static (m, s) => ReadMessage(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext);
static PropertyChanges<StatusNotifierWatcherProperties> ReadMessage(Message message, StatusNotifierWatcherObject _)
{
var reader = message.GetBodyReader();
reader.ReadString(); // interface
List<string> changed = new();
return new PropertyChanges<StatusNotifierWatcherProperties>(ReadProperties(ref reader, changed), changed.ToArray(),
ReadInvalidated(ref reader));
}
static string[] ReadInvalidated(ref Reader reader)
{
List<string>? invalidated = null;
var headersEnd = reader.ReadArrayStart(DBusType.String);
while (reader.HasNext(headersEnd))
{
invalidated ??= new List<string>();
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<string>();
}
}
private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, List<string>? 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<string>();
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<IDisposable> WatchPropertiesChangedAsync<TProperties>(string @interface, MessageValueReader<PropertyChanges<TProperties>> reader, Action<Exception?, PropertyChanges<TProperties>> 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<Exception?, PropertyChanges<TProperties>>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync<TArg>(string sender, string @interface, ObjectPath path, string signal, MessageValueReader<TArg> reader, Action<Exception?, TArg> 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<Exception?, TArg>)hs!).Invoke(ex, arg),
this, handler, emitOnCapturedContext);
}
public ValueTask<IDisposable> WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action<Exception?> 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<Exception?>)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<string>();
}
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();
}
}
}

6
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<Task<IStorageProvider>>[]
StorageProvider = new CompositeStorageProvider(new[]
{
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult<IStorageProvider>(null),
() => GtkSystemDialog.TryCreate(this),
() => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult<IStorageProvider>(null),
() => GtkSystemDialog.TryCreate(this)
});
}

1
src/Linux/Tmds.DBus

@ -0,0 +1 @@
Subproject commit bfca94ab052683c7ccef51e4a036098f539cc676
Loading…
Cancel
Save