csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
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.
278 lines
9.5 KiB
278 lines
9.5 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Controls.Platform;
|
|
using Avalonia.Logging;
|
|
using Avalonia.Platform;
|
|
using Tmds.DBus.Protocol;
|
|
using Tmds.DBus.SourceGenerator;
|
|
|
|
namespace Avalonia.FreeDesktop
|
|
{
|
|
internal class DBusTrayIconImpl : ITrayIconImpl
|
|
{
|
|
private static int s_trayIconInstanceId;
|
|
public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 });
|
|
|
|
private readonly ObjectPath _dbusMenuPath;
|
|
private readonly Connection? _connection;
|
|
private readonly OrgFreedesktopDBus? _dBus;
|
|
|
|
private IDisposable? _serviceWatchDisposable;
|
|
private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
|
|
private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher;
|
|
private (int, int, byte[]) _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;
|
|
|
|
_dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus");
|
|
_dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
|
|
|
|
MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
|
|
|
|
_statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath);
|
|
_connection.AddMethodHandler(_statusNotifierItemDbusObj);
|
|
|
|
WatchAsync();
|
|
}
|
|
|
|
private async void WatchAsync()
|
|
{
|
|
try
|
|
{
|
|
_serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item1, x.Item3));
|
|
var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher");
|
|
OnNameChange("org.kde.StatusNotifierWatcher", nameOwner);
|
|
}
|
|
catch
|
|
{
|
|
_serviceWatchDisposable = null;
|
|
Logger.TryGet(LogEventLevel.Error, "DBUS")
|
|
?.Log(this, "Interface 'org.kde.StatusNotifierWatcher' is unavailable.");
|
|
}
|
|
}
|
|
|
|
private void OnNameChange(string name, string? newOwner)
|
|
{
|
|
if (_isDisposed || _connection is null || name != "org.kde.StatusNotifierWatcher")
|
|
return;
|
|
|
|
if (!_serviceConnected & newOwner is not null)
|
|
{
|
|
_serviceConnected = true;
|
|
_statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher");
|
|
|
|
DestroyTrayIcon();
|
|
|
|
if (_isVisible)
|
|
CreateTrayIcon();
|
|
}
|
|
else if (_serviceConnected & newOwner is null)
|
|
{
|
|
DestroyTrayIcon();
|
|
_serviceConnected = false;
|
|
}
|
|
}
|
|
|
|
private async void CreateTrayIcon()
|
|
{
|
|
if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierWatcher is null)
|
|
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}");
|
|
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 || _sysTrayServiceName is null)
|
|
return;
|
|
|
|
_dBus!.ReleaseNameAsync(_sysTrayServiceName);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
IsActive = false;
|
|
_isDisposed = true;
|
|
DestroyTrayIcon();
|
|
_serviceWatchDisposable?.Dispose();
|
|
}
|
|
|
|
public void SetIcon(IWindowIconImpl? icon)
|
|
{
|
|
if (_isDisposed || IconConverterDelegate is null)
|
|
return;
|
|
|
|
if (icon is null)
|
|
{
|
|
_statusNotifierItemDbusObj?.SetIcon(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 = (w, h, pixByteArray);
|
|
_statusNotifierItemDbusObj?.SetIcon(_icon);
|
|
}
|
|
|
|
public void SetIsVisible(bool visible)
|
|
{
|
|
if (_isDisposed || !_serviceConnected)
|
|
{
|
|
_isVisible = visible;
|
|
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 : OrgKdeStatusNotifierItem
|
|
{
|
|
public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath)
|
|
{
|
|
Connection = connection;
|
|
BackingProperties.Menu = dbusMenuPath;
|
|
BackingProperties.Category = string.Empty;
|
|
BackingProperties.Status = string.Empty;
|
|
BackingProperties.Id = string.Empty;
|
|
BackingProperties.Title = string.Empty;
|
|
BackingProperties.IconPixmap = Array.Empty<(int, int, byte[])>();
|
|
BackingProperties.AttentionIconName = string.Empty;
|
|
BackingProperties.AttentionIconPixmap = Array.Empty<(int, int, byte[])>();
|
|
BackingProperties.AttentionMovieName = string.Empty;
|
|
BackingProperties.OverlayIconName = string.Empty;
|
|
BackingProperties.OverlayIconPixmap = Array.Empty<(int, int, byte[])>();
|
|
BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty);
|
|
InvalidateAll();
|
|
}
|
|
|
|
protected override Connection Connection { get; }
|
|
|
|
public override string Path => "/StatusNotifierItem";
|
|
|
|
public event Action? ActivationDelegate;
|
|
|
|
protected override ValueTask OnContextMenuAsync(int x, int y) => new();
|
|
|
|
protected override ValueTask OnActivateAsync(int x, int y)
|
|
{
|
|
ActivationDelegate?.Invoke();
|
|
return new ValueTask();
|
|
}
|
|
|
|
protected override ValueTask OnSecondaryActivateAsync(int x, int y) => new();
|
|
|
|
protected override ValueTask OnScrollAsync(int delta, string orientation) => new();
|
|
|
|
public void InvalidateAll()
|
|
{
|
|
EmitNewTitle();
|
|
EmitNewIcon();
|
|
EmitNewAttentionIcon();
|
|
EmitNewOverlayIcon();
|
|
EmitNewToolTip();
|
|
EmitNewStatus(BackingProperties.Status);
|
|
}
|
|
|
|
public void SetIcon((int, int, byte[]) 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;
|
|
InvalidateAll();
|
|
}
|
|
}
|
|
}
|
|
|