A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

453 lines
15 KiB

#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.Logging;
using Avalonia.Platform;
using Tmds.DBus;
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
namespace Avalonia.FreeDesktop
{
internal class DBusTrayIconImpl : ITrayIconImpl
{
private static int s_trayIconInstanceId;
private readonly ObjectPath _dbusMenuPath;
private readonly Connection? _connection;
private IDisposable? _serviceWatchDisposable;
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
private IStatusNotifierWatcher? _statusNotifierWatcher;
private DbusPixmap _icon;
private string? _sysTrayServiceName;
private string? _tooltipText;
private bool _isDisposed;
private bool _serviceConnected;
private bool _isVisible = true;
public bool IsActive { get; private set; }
public INativeMenuExporter? MenuExporter { get; }
public Action? OnClicked { get; set; }
public Func<IWindowIconImpl?, uint[]>? IconConverterDelegate { get; set; }
public DBusTrayIconImpl()
{
_connection = DBusHelper.TryCreateNewConnection();
if (_connection is null)
{
Logger.TryGet(LogEventLevel.Error, "DBUS")
?.Log(this, "Unable to get a dbus connection for system tray icons.");
return;
}
IsActive = true;
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
WatchAsync();
}
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.");
return;
}
_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}");
}
}
private void OnNameChange(ServiceOwnerChangedEventArgs obj)
{
if (_isDisposed)
return;
if (!_serviceConnected & obj.NewOwner != null)
{
_serviceConnected = true;
InitializeSNWService();
DestroyTrayIcon();
if (_isVisible)
{
CreateTrayIcon();
}
}
else if (_serviceConnected & obj.NewOwner is null)
{
DestroyTrayIcon();
_serviceConnected = false;
}
}
private void CreateTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed)
return;
var pid = Process.GetCurrentProcess().Id;
var tid = s_trayIconInstanceId++;
_sysTrayServiceName = $"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}.");
_serviceConnected = false;
}
_statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
_statusNotifierItemDbusObj.SetIcon(_icon);
_statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
}
private void DestroyTrayIcon()
{
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null)
return;
_connection.UnregisterObject(_statusNotifierItemDbusObj);
_connection.UnregisterServiceAsync(_sysTrayServiceName);
}
public void Dispose()
{
IsActive = false;
_isDisposed = true;
DestroyTrayIcon();
_connection?.Dispose();
_serviceWatchDisposable?.Dispose();
}
public void SetIcon(IWindowIconImpl? icon)
{
if (_isDisposed || IconConverterDelegate is null)
return;
if (icon is null)
{
_statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap);
return;
}
var x11iconData = IconConverterDelegate(icon);
if (x11iconData.Length == 0) 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];
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)
return;
switch (visible)
{
case true when !_isVisible:
DestroyTrayIcon();
CreateTrayIcon();
break;
case false when _isVisible:
DestroyTrayIcon();
break;
}
_isVisible = visible;
}
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)
{
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)
{
_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);
}
// 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 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;
}
public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 });
}
}