3 changed files with 462 additions and 311 deletions
@ -0,0 +1,358 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Reactive.Disposables; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.FreeDesktop; |
||||
|
using Avalonia.Logging; |
||||
|
using Avalonia.Platform; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop |
||||
|
{ |
||||
|
public class DbusSNITrayIconImpl |
||||
|
{ |
||||
|
private static int s_trayIconInstanceId; |
||||
|
private readonly ObjectPath _dbusMenuPath; |
||||
|
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; |
||||
|
private readonly Connection? _connection; |
||||
|
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 bool IsActive => _isActive; |
||||
|
|
||||
|
public DbusSNITrayIconImpl(Connection connection) |
||||
|
{ |
||||
|
_connection = connection; |
||||
|
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; |
||||
|
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); |
||||
|
CreateTrayIcon(); |
||||
|
_ctorFinished = true; |
||||
|
} |
||||
|
|
||||
|
public async void CreateTrayIcon() |
||||
|
{ |
||||
|
if (_connection is null) |
||||
|
return; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
_statusNotifierWatcher = _connection.CreateProxy<IStatusNotifierWatcher>( |
||||
|
"org.kde.StatusNotifierWatcher", |
||||
|
"/StatusNotifierWatcher"); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) |
||||
|
?.Log(this, |
||||
|
"DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); |
||||
|
} |
||||
|
|
||||
|
if (_statusNotifierWatcher is null) |
||||
|
return; |
||||
|
|
||||
|
var pid = Process.GetCurrentProcess().Id; |
||||
|
var tid = s_trayIconInstanceId++; |
||||
|
|
||||
|
_sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; |
||||
|
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); |
||||
|
|
||||
|
await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); |
||||
|
|
||||
|
await _connection.RegisterServiceAsync(_sysTrayServiceName); |
||||
|
|
||||
|
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); |
||||
|
|
||||
|
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); |
||||
|
_statusNotifierItemDbusObj.SetIcon(_icon); |
||||
|
|
||||
|
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked; |
||||
|
|
||||
|
_isActive = true; |
||||
|
} |
||||
|
|
||||
|
public async void DestroyTrayIcon() |
||||
|
{ |
||||
|
if (_connection is null) |
||||
|
return; |
||||
|
_connection.UnregisterObject(_statusNotifierItemDbusObj); |
||||
|
await _connection.UnregisterServiceAsync(_sysTrayServiceName); |
||||
|
_isActive = false; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
_isDisposed = true; |
||||
|
DestroyTrayIcon(); |
||||
|
_connection?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
public void SetIcon(UIntPtr[] x11iconData) |
||||
|
{ |
||||
|
if (_isDisposed) |
||||
|
return; |
||||
|
var w = (int)x11iconData[0]; |
||||
|
var h = (int)x11iconData[1]; |
||||
|
|
||||
|
var pixLength = w * h; |
||||
|
var pixByteArrayCounter = 0; |
||||
|
var pixByteArray = new byte[w * h * 4]; |
||||
|
|
||||
|
for (var i = 0; i < pixLength; i++) |
||||
|
{ |
||||
|
var rawPixel = x11iconData[i + 2].ToUInt32(); |
||||
|
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); |
||||
|
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); |
||||
|
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); |
||||
|
pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); |
||||
|
} |
||||
|
|
||||
|
_icon = new DbusPixmap(w, h, pixByteArray); |
||||
|
_statusNotifierItemDbusObj?.SetIcon(_icon); |
||||
|
} |
||||
|
|
||||
|
public void SetIsVisible(bool visible) |
||||
|
{ |
||||
|
if (_isDisposed || !_ctorFinished) |
||||
|
return; |
||||
|
|
||||
|
if (visible & !_isActive) |
||||
|
{ |
||||
|
DestroyTrayIcon(); |
||||
|
CreateTrayIcon(); |
||||
|
} |
||||
|
else if (!visible & _isActive) |
||||
|
{ |
||||
|
DestroyTrayIcon(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SetToolTipText(string? text) |
||||
|
{ |
||||
|
if (_isDisposed || text is null) |
||||
|
return; |
||||
|
_tooltipText = text; |
||||
|
_statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// DBus Object used for setting system tray icons.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
|
||||
|
/// </remarks>
|
||||
|
internal class StatusNotifierItemDbusObj : IStatusNotifierItem |
||||
|
{ |
||||
|
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"); |
||||
|
|
||||
|
_backingProperties = new StatusNotifierItemProperties |
||||
|
{ |
||||
|
Menu = dbusmenuPath, // Needs a dbus menu somehow
|
||||
|
ToolTip = new ToolTip("") |
||||
|
}; |
||||
|
|
||||
|
InvalidateAll(); |
||||
|
} |
||||
|
|
||||
|
public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; |
||||
|
|
||||
|
public Task ActivateAsync(int x, int y) |
||||
|
{ |
||||
|
ActivationDelegate?.Invoke(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; |
||||
|
|
||||
|
public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; |
||||
|
|
||||
|
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)); |
||||
|
} |
||||
|
|
||||
|
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) => Task.FromResult(new object()); |
||||
|
|
||||
|
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) |
||||
|
{ |
||||
|
_backingProperties.IconPixmap = new[] { dbusPixmap }; |
||||
|
InvalidateAll(); |
||||
|
} |
||||
|
|
||||
|
public void SetTitleAndTooltip(string? text) |
||||
|
{ |
||||
|
if (text is null) |
||||
|
return; |
||||
|
|
||||
|
_backingProperties.Id = text; |
||||
|
_backingProperties.Category = "ApplicationStatus"; |
||||
|
_backingProperties.Status = text; |
||||
|
_backingProperties.Title = text; |
||||
|
_backingProperties.ToolTip = new ToolTip(text); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
[Dictionary] |
||||
|
// 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.
|
||||
|
internal class StatusNotifierItemProperties |
||||
|
{ |
||||
|
public string? Category; |
||||
|
|
||||
|
public string? Id; |
||||
|
|
||||
|
public string? Title; |
||||
|
|
||||
|
public string? Status; |
||||
|
|
||||
|
public ObjectPath Menu; |
||||
|
|
||||
|
public DbusPixmap[]? IconPixmap; |
||||
|
|
||||
|
public ToolTip ToolTip; |
||||
|
} |
||||
|
|
||||
|
internal struct ToolTip |
||||
|
{ |
||||
|
public readonly string First; |
||||
|
public readonly DbusPixmap[] Second; |
||||
|
public readonly string Third; |
||||
|
public readonly string Fourth; |
||||
|
|
||||
|
private static readonly DbusPixmap[] s_blank = |
||||
|
{ |
||||
|
new DbusPixmap(0, 0, Array.Empty<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>()) |
||||
|
}; |
||||
|
|
||||
|
public ToolTip(string message) : this("", s_blank, message, "") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public ToolTip(string first, DbusPixmap[] second, string third, string fourth) |
||||
|
{ |
||||
|
First = first; |
||||
|
Second = second; |
||||
|
Third = third; |
||||
|
Fourth = fourth; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal readonly struct DbusPixmap |
||||
|
{ |
||||
|
public readonly int Width; |
||||
|
public readonly int Height; |
||||
|
public readonly byte[] Data; |
||||
|
|
||||
|
public DbusPixmap(int width, int height, byte[] data) |
||||
|
{ |
||||
|
Width = width; |
||||
|
Height = height; |
||||
|
Data = data; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,367 +1,124 @@ |
|||||
#nullable enable |
|
||||
|
|
||||
using System; |
using System; |
||||
using System.Diagnostics; |
|
||||
using System.Reactive.Disposables; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Controls.Platform; |
using Avalonia.Controls.Platform; |
||||
using Avalonia.FreeDesktop; |
using Avalonia.FreeDesktop; |
||||
using Avalonia.Logging; |
using Avalonia.Logging; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
using Tmds.DBus; |
|
||||
|
|
||||
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] |
|
||||
|
|
||||
namespace Avalonia.X11 |
namespace Avalonia.X11 |
||||
{ |
{ |
||||
internal class X11TrayIconImpl : ITrayIconImpl |
internal class X11TrayIconImpl : ITrayIconImpl |
||||
{ |
{ |
||||
private static int s_trayIconInstanceId; |
|
||||
private readonly ObjectPath _dbusMenuPath; |
|
||||
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; |
|
||||
private readonly Connection? _connection; |
|
||||
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() |
public X11TrayIconImpl() |
||||
{ |
{ |
||||
_connection = DBusHelper.TryGetConnection(); |
_xEmbedTrayIcon = new XEmbedTrayIconImpl(); |
||||
|
|
||||
|
var _connection = DBusHelper.TryGetConnection(); |
||||
|
|
||||
if (_connection is null) |
if (_connection is null) |
||||
{ |
{ |
||||
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) |
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) |
||||
?.Log(this, "Unable to get a dbus connection for system tray icons."); |
?.Log(this, "Unable to get a dbus connection for system tray icons."); |
||||
return; |
|
||||
} |
} |
||||
|
|
||||
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; |
_dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); |
||||
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); |
|
||||
CreateTrayIcon(); |
|
||||
_ctorFinished = true; |
|
||||
} |
} |
||||
|
|
||||
public async void CreateTrayIcon() |
private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; |
||||
{ |
|
||||
if (_connection is null) |
|
||||
return; |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
_statusNotifierWatcher = _connection.CreateProxy<IStatusNotifierWatcher>( |
|
||||
"org.kde.StatusNotifierWatcher", |
|
||||
"/StatusNotifierWatcher"); |
|
||||
} |
|
||||
catch |
|
||||
{ |
|
||||
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) |
|
||||
?.Log(this, |
|
||||
"DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it."); |
|
||||
} |
|
||||
|
|
||||
if (_statusNotifierWatcher is null) |
|
||||
return; |
|
||||
|
|
||||
var pid = Process.GetCurrentProcess().Id; |
|
||||
var tid = s_trayIconInstanceId++; |
|
||||
|
|
||||
_sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}"; |
|
||||
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); |
|
||||
|
|
||||
await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); |
|
||||
|
|
||||
await _connection.RegisterServiceAsync(_sysTrayServiceName); |
|
||||
|
|
||||
await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); |
|
||||
|
|
||||
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); |
|
||||
_statusNotifierItemDbusObj.SetIcon(_icon); |
|
||||
|
|
||||
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked; |
private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; |
||||
|
private bool _isDisposed; |
||||
_isActive = true; |
|
||||
} |
|
||||
|
|
||||
public async void DestroyTrayIcon() |
|
||||
{ |
|
||||
if (_connection is null) |
|
||||
return; |
|
||||
_connection.UnregisterObject(_statusNotifierItemDbusObj); |
|
||||
await _connection.UnregisterServiceAsync(_sysTrayServiceName); |
|
||||
_isActive = false; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
public void Dispose() |
||||
{ |
{ |
||||
|
_dbusSniTrayIcon?.Dispose(); |
||||
|
_xEmbedTrayIcon?.Dispose(); |
||||
_isDisposed = true; |
_isDisposed = true; |
||||
DestroyTrayIcon(); |
|
||||
_connection?.Dispose(); |
|
||||
} |
} |
||||
|
|
||||
public void SetIcon(IWindowIconImpl? icon) |
public void SetIcon(IWindowIconImpl? icon) |
||||
{ |
{ |
||||
if (_isDisposed) |
if (_isDisposed) return; |
||||
return; |
|
||||
if (!(icon is X11IconData x11icon)) |
|
||||
return; |
|
||||
|
|
||||
var w = (int)x11icon.Data[0]; |
if (_dbusSniTrayIcon?.IsActive ?? false) |
||||
var h = (int)x11icon.Data[1]; |
{ |
||||
|
if (!(icon is X11IconData x11icon)) |
||||
var pixLength = w * h; |
return; |
||||
var pixByteArrayCounter = 0; |
|
||||
var pixByteArray = new byte[w * h * 4]; |
|
||||
|
|
||||
for (var i = 0; i < pixLength; i++) |
_dbusSniTrayIcon.SetIcon(x11icon.Data); |
||||
|
} |
||||
|
else |
||||
{ |
{ |
||||
var rawPixel = x11icon.Data[i + 2].ToUInt32(); |
_xEmbedTrayIcon.SetIcon(icon); |
||||
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24); |
|
||||
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16); |
|
||||
pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8); |
|
||||
pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); |
|
||||
} |
} |
||||
|
|
||||
_icon = new DbusPixmap(w, h, pixByteArray); |
|
||||
_statusNotifierItemDbusObj?.SetIcon(_icon); |
|
||||
} |
} |
||||
|
|
||||
public void SetIsVisible(bool visible) |
public void SetToolTipText(string? text) |
||||
{ |
{ |
||||
if (_isDisposed || !_ctorFinished) |
if (_isDisposed) return; |
||||
return; |
|
||||
|
|
||||
if (visible & !_isActive) |
if (_dbusSniTrayIcon?.IsActive ?? false) |
||||
{ |
{ |
||||
DestroyTrayIcon(); |
_dbusSniTrayIcon.SetToolTipText(text); |
||||
CreateTrayIcon(); |
|
||||
} |
} |
||||
else if (!visible & _isActive) |
else |
||||
{ |
{ |
||||
DestroyTrayIcon(); |
_xEmbedTrayIcon.SetToolTipText(text); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
public void SetToolTipText(string? text) |
public void SetIsVisible(bool visible) |
||||
{ |
|
||||
if (_isDisposed || text is null) |
|
||||
return; |
|
||||
_tooltipText = text; |
|
||||
_statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// DBus Object used for setting system tray icons.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
|
|
||||
/// </remarks>
|
|
||||
internal class StatusNotifierItemDbusObj : IStatusNotifierItem |
|
||||
{ |
|
||||
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"); |
if (_isDisposed) return; |
||||
|
|
||||
_backingProperties = new StatusNotifierItemProperties |
if (_dbusSniTrayIcon?.IsActive ?? false) |
||||
{ |
{ |
||||
Menu = dbusmenuPath, // Needs a dbus menu somehow
|
_dbusSniTrayIcon.SetIsVisible(visible); |
||||
ToolTip = new ToolTip("") |
} |
||||
}; |
else |
||||
|
{ |
||||
InvalidateAll(); |
_xEmbedTrayIcon.SetIsVisible(visible); |
||||
} |
} |
||||
|
|
||||
public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; |
|
||||
|
|
||||
public Task ActivateAsync(int x, int y) |
|
||||
{ |
|
||||
ActivationDelegate?.Invoke(); |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; |
|
||||
|
|
||||
public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; |
|
||||
|
|
||||
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)); |
|
||||
} |
|
||||
|
|
||||
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) => Task.FromResult(new object()); |
|
||||
|
|
||||
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) |
|
||||
{ |
|
||||
_backingProperties.IconPixmap = new[] { dbusPixmap }; |
|
||||
InvalidateAll(); |
|
||||
} |
|
||||
|
|
||||
public void SetTitleAndTooltip(string? text) |
|
||||
{ |
|
||||
if (text is null) |
|
||||
return; |
|
||||
|
|
||||
_backingProperties.Id = text; |
|
||||
_backingProperties.Category = "ApplicationStatus"; |
|
||||
_backingProperties.Status = text; |
|
||||
_backingProperties.Title = text; |
|
||||
_backingProperties.ToolTip = new ToolTip(text); |
|
||||
|
|
||||
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); |
|
||||
} |
|
||||
|
|
||||
[Dictionary] |
|
||||
// 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.
|
|
||||
internal class StatusNotifierItemProperties |
|
||||
{ |
|
||||
public string? Category; |
|
||||
|
|
||||
public string? Id; |
|
||||
|
|
||||
public string? Title; |
|
||||
|
|
||||
public string? Status; |
|
||||
|
|
||||
public ObjectPath Menu; |
|
||||
|
|
||||
public DbusPixmap[]? IconPixmap; |
|
||||
|
|
||||
public ToolTip ToolTip; |
|
||||
} |
|
||||
|
|
||||
internal struct ToolTip |
|
||||
{ |
|
||||
public readonly string First; |
|
||||
public readonly DbusPixmap[] Second; |
|
||||
public readonly string Third; |
|
||||
public readonly string Fourth; |
|
||||
|
|
||||
private static readonly DbusPixmap[] s_blank = |
|
||||
{ |
|
||||
new DbusPixmap(0, 0, Array.Empty<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>()) |
|
||||
}; |
|
||||
|
|
||||
public ToolTip(string message) : this("", s_blank, message, "") |
|
||||
{ |
|
||||
} |
} |
||||
|
|
||||
public ToolTip(string first, DbusPixmap[] second, string third, string fourth) |
public INativeMenuExporter? MenuExporter |
||||
{ |
{ |
||||
First = first; |
get |
||||
Second = second; |
{ |
||||
Third = third; |
if (_dbusSniTrayIcon?.IsActive ?? false) |
||||
Fourth = fourth; |
{ |
||||
|
return _dbusSniTrayIcon.MenuExporter; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return _xEmbedTrayIcon.MenuExporter; |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
} |
|
||||
|
|
||||
internal readonly struct DbusPixmap |
|
||||
{ |
|
||||
public readonly int Width; |
|
||||
public readonly int Height; |
|
||||
public readonly byte[] Data; |
|
||||
|
|
||||
public DbusPixmap(int width, int height, byte[] data) |
public Action? OnClicked |
||||
{ |
{ |
||||
Width = width; |
get |
||||
Height = height; |
{ |
||||
Data = data; |
if (_dbusSniTrayIcon?.IsActive ?? false) |
||||
|
{ |
||||
|
return _dbusSniTrayIcon.OnClicked; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return _xEmbedTrayIcon.OnClicked; |
||||
|
} |
||||
|
} |
||||
|
set |
||||
|
{ |
||||
|
if (_dbusSniTrayIcon?.IsActive ?? false) |
||||
|
{ |
||||
|
_dbusSniTrayIcon.OnClicked = value; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_xEmbedTrayIcon.OnClicked = value; |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,36 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls.Platform; |
||||
|
using Avalonia.Logging; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.X11 |
||||
|
{ |
||||
|
internal class XEmbedTrayIconImpl |
||||
|
{ |
||||
|
public XEmbedTrayIconImpl() |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) |
||||
|
?.Log(this, |
||||
|
"TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system."); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void SetIcon(IWindowIconImpl? icon) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void SetToolTipText(string? text) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void SetIsVisible(bool visible) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public INativeMenuExporter? MenuExporter { get; } |
||||
|
public Action? OnClicked { get; set; } |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue