diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs
index 2ad220dddd..08ed8669ef 100644
--- a/src/Avalonia.Base/Logging/LogArea.cs
+++ b/src/Avalonia.Base/Logging/LogArea.cs
@@ -39,5 +39,10 @@ namespace Avalonia.Logging
/// The log event comes from Win32Platform.
///
public const string Win32Platform = nameof(Win32Platform);
+
+ ///
+ /// The log event comes from Win32Platform.
+ ///
+ public const string X11Platform = nameof(X11Platform);
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs
index 7996a94dd0..4e23711ed4 100644
--- a/src/Avalonia.FreeDesktop/DBusHelper.cs
+++ b/src/Avalonia.FreeDesktop/DBusHelper.cs
@@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop
public static Connection TryInitialize(string dbusAddress = null)
{
- if (Connection != null)
- return Connection;
+ return Connection ?? TryGetConnection(dbusAddress);
+ }
+
+ public static Connection TryGetConnection(string dbusAddress = null)
+ {
var oldContext = SynchronizationContext.Current;
try
{
diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs
index b5e35db969..932b876088 100644
--- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs
+++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs
@@ -8,6 +8,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.FreeDesktop.DBusMenu;
using Avalonia.Input;
+using Avalonia.Platform;
using Avalonia.Threading;
using Tmds.DBus;
#pragma warning disable 1998
@@ -16,15 +17,23 @@ namespace Avalonia.FreeDesktop
{
public class DBusMenuExporter
{
- public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid)
+ public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid)
{
if (DBusHelper.Connection == null)
return null;
return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
}
+
+ public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection)
+ {
+ return new DBusMenuExporterImpl(currentConection, path);
+ }
+
+ public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/"
+ + Guid.NewGuid().ToString().Replace("-", "");
- class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
+ private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
{
private readonly Connection _dbus;
private readonly uint _xid;
@@ -37,30 +46,48 @@ namespace Avalonia.FreeDesktop
private readonly HashSet _menus = new HashSet();
private bool _resetQueued;
private int _nextId = 1;
+ private bool AppMenu = true;
public DBusMenuExporterImpl(Connection dbus, IntPtr xid)
{
_dbus = dbus;
_xid = (uint)xid.ToInt32();
- ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/"
- + Guid.NewGuid().ToString().Replace("-", ""));
+ ObjectPath = GenerateDBusMenuObjPath;
SetNativeMenu(new NativeMenu());
Init();
}
+ public DBusMenuExporterImpl(Connection dbus, ObjectPath path)
+ {
+ _dbus = dbus;
+ AppMenu = false;
+ ObjectPath = path;
+ SetNativeMenu(new NativeMenu());
+ Init();
+ }
+
async void Init()
{
try
{
- await _dbus.RegisterObjectAsync(this);
- _registar = DBusHelper.Connection.CreateProxy(
- "com.canonical.AppMenu.Registrar",
- "/com/canonical/AppMenu/Registrar");
- if (!_disposed)
- await _registar.RegisterWindowAsync(_xid, ObjectPath);
+ if (AppMenu)
+ {
+ await _dbus.RegisterObjectAsync(this);
+ _registar = DBusHelper.Connection.CreateProxy(
+ "com.canonical.AppMenu.Registrar",
+ "/com/canonical/AppMenu/Registrar");
+ if (!_disposed)
+ await _registar.RegisterWindowAsync(_xid, ObjectPath);
+ }
+ else
+ {
+ await _dbus.RegisterObjectAsync(this);
+ }
}
catch (Exception e)
{
- Console.Error.WriteLine(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
@@ -248,17 +275,24 @@ namespace Avalonia.FreeDesktop
if (item.ToggleType != NativeMenuItemToggleType.None)
return item.IsChecked ? 1 : 0;
}
-
+
if (name == "icon-data")
{
if (item.Icon != null)
{
- var ms = new MemoryStream();
- item.Icon.Save(ms);
- return ms.ToArray();
+ var loader = AvaloniaLocator.Current.GetService();
+
+ if (loader != null)
+ {
+ var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item);
+
+ using var ms = new MemoryStream();
+ icon.Save(ms);
+ return ms.ToArray();
+ }
}
}
-
+
if (name == "children-display")
return menu != null ? "submenu" : null;
}
diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs
index 8909a4604b..99a850dabb 100644
--- a/src/Avalonia.X11/X11TrayIconImpl.cs
+++ b/src/Avalonia.X11/X11TrayIconImpl.cs
@@ -1,33 +1,374 @@
using System;
+using System.Diagnostics;
+using System.Reactive.Disposables;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using Avalonia.Controls.Platform;
+using Avalonia.FreeDesktop;
using Avalonia.Platform;
+using Tmds.DBus;
+
+[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)]
namespace Avalonia.X11
{
- class X11TrayIconImpl : ITrayIconImpl
+ internal class X11TrayIconImpl : ITrayIconImpl
{
- public INativeMenuExporter MenuExporter => null;
+
+ private static int s_trayIconInstanceId = 0;
+ private static int GetTID() => s_trayIconInstanceId++;
+ private ObjectPath _dbusmenuPath;
+ private StatusNotifierItemDbusObj _statusNotifierItemDbusObj;
+ private Connection con;
+ private DbusPixmap _icon;
+
+ private IStatusNotifierWatcher _statusNotifierWatcher;
+ private string _sysTrayServiceName;
+ private string _tooltipText;
+ private bool _isActive;
+ private bool _isDisposed;
+ private readonly bool _ctorFinished;
+
+ public INativeMenuExporter MenuExporter { get; }
public Action OnClicked { get; set; }
+
+ public X11TrayIconImpl()
+ {
+ con = DBusHelper.TryGetConnection();
+ _dbusmenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
+ MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusmenuPath, con);
+ CreateTrayIcon();
+ _ctorFinished = true;
+ }
+
+ public async void CreateTrayIcon()
+ {
+ _statusNotifierWatcher = con.CreateProxy("org.kde.StatusNotifierWatcher",
+ "/StatusNotifierWatcher");
+
+ var pid = Process.GetCurrentProcess().Id;
+ var tid = GetTID();
+
+ _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}";
+ _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusmenuPath);
+
+ await con.RegisterObjectAsync(_statusNotifierItemDbusObj);
+
+ await con.RegisterServiceAsync(_sysTrayServiceName);
+
+ await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
+
+ _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
+ _statusNotifierItemDbusObj.SetIcon(_icon);
+
+ _statusNotifierItemDbusObj.ActivationDelegate = () =>
+ {
+ OnClicked?.Invoke();
+ };
+
+ _isActive = true;
+ }
+
+ public async void DestroyTrayIcon()
+ {
+ con.UnregisterObject(_statusNotifierItemDbusObj);
+ await con.UnregisterServiceAsync(_sysTrayServiceName);
+ _isActive = false;
+ }
public void Dispose()
{
-
+ _isDisposed = true;
+ DestroyTrayIcon();
+ con.Dispose();
}
public void SetIcon(IWindowIconImpl icon)
{
-
- }
+ if (con == null || _isDisposed) return;
+ if (!(icon is X11IconData x11icon)) return;
+
+ var w = (int)x11icon.Data[0];
+ var h = (int)x11icon.Data[1];
+
+ var rx = x11icon.Data.AsSpan(2);
+ var pixLength = w * h;
+
+ var pixByteArrayCounter = 0;
+ var pixByteArray = new byte[w * h * 4];
+ for (var i = 0; i < pixLength; i++)
+ {
+ var u = rx[i].ToUInt32();
+ pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF000000) >> 24);
+ pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF0000) >> 16);
+ pixByteArray[pixByteArrayCounter++] = (byte)((u & 0xFF00) >> 8);
+ pixByteArray[pixByteArrayCounter++] = (byte)(u & 0xFF);
+ }
+
+ _icon = new DbusPixmap(w, h, pixByteArray);
+ _statusNotifierItemDbusObj.SetIcon(_icon);
+ }
+
public void SetIsVisible(bool visible)
{
-
+ if (con == null || _isDisposed || !_ctorFinished) return;
+
+ if (visible & !_isActive)
+ {
+ DestroyTrayIcon();
+ CreateTrayIcon();
+ }
+ else if (!visible & _isActive)
+ {
+ DestroyTrayIcon();
+ }
}
public void SetToolTipText(string text)
{
-
+ if (con == null || _isDisposed) return;
+ _tooltipText = text;
+ _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText);
+ }
+ }
+
+ ///
+ /// DBus Object used for setting system tray icons.
+ ///
+ ///
+ /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
+ ///
+ internal class StatusNotifierItemDbusObj : IStatusNotifierItem
+ {
+ private readonly StatusNotifierItemProperties _backingProperties;
+ private event Action OnPropertyChange;
+ public event Action OnTitleChanged;
+ public event Action OnIconChanged;
+ public event Action OnAttentionIconChanged;
+ public event Action OnOverlayIconChanged;
+ public event Action OnTooltipChanged;
+ public Action SetNativeMenuExporter { get; set; }
+ public Action NewStatusAsync { get; set; }
+ public Action ActivationDelegate { get; set; }
+ public ObjectPath ObjectPath { get; }
+
+ public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath)
+ {
+ ObjectPath = new ObjectPath($"/StatusNotifierItem");
+
+ _backingProperties = new StatusNotifierItemProperties
+ {
+ Menu = dbusmenuPath, // Needs a dbus menu somehow
+ ToolTip = new ToolTip("")
+ };
+
+ InvalidateAll();
+ }
+
+ public async Task ContextMenuAsync(int X, int Y)
+ {
+ }
+
+ public async Task ActivateAsync(int X, int Y)
+ {
+ ActivationDelegate?.Invoke();
+ }
+
+ public async Task SecondaryActivateAsync(int X, int Y)
+ {
+ }
+
+ public async Task ScrollAsync(int Delta, string Orientation)
+ {
+ }
+
+ public void InvalidateAll()
+ {
+ OnTitleChanged?.Invoke();
+ OnIconChanged?.Invoke();
+ OnOverlayIconChanged?.Invoke();
+ OnAttentionIconChanged?.Invoke();
+ OnTooltipChanged?.Invoke();
+ }
+
+ public async Task WatchNewTitleAsync(Action handler, Action onError = null)
+ {
+ OnTitleChanged += handler;
+ return Disposable.Create(() => OnTitleChanged -= handler);
+ }
+
+ public async Task WatchNewIconAsync(Action handler, Action onError = null)
+ {
+ OnIconChanged += handler;
+ return Disposable.Create(() => OnIconChanged -= handler);
+ }
+
+ public async Task WatchNewAttentionIconAsync(Action handler, Action onError = null)
+ {
+ OnAttentionIconChanged += handler;
+ return Disposable.Create(() => OnAttentionIconChanged -= handler);
+ }
+
+ public async Task WatchNewOverlayIconAsync(Action handler, Action onError = null)
+ {
+ OnOverlayIconChanged += handler;
+ return Disposable.Create(() => OnOverlayIconChanged -= handler);
+ }
+
+ public async Task WatchNewToolTipAsync(Action handler, Action onError = null)
+ {
+ OnTooltipChanged += handler;
+ return Disposable.Create(() => OnTooltipChanged -= handler);
+ }
+
+ public async Task WatchNewStatusAsync(Action handler, Action onError = null)
+ {
+ NewStatusAsync += handler;
+ return Disposable.Create(() => NewStatusAsync -= handler);
+ }
+
+ public async Task