Browse Source

Merge remote-tracking branch 'origin/feature/tray-icon-dbus-sni-test' into feature/tray-icon-support

pull/6560/head
Dan Walmsley 5 years ago
parent
commit
fbb220675d
  1. 5
      src/Avalonia.Base/Logging/LogArea.cs
  2. 7
      src/Avalonia.FreeDesktop/DBusHelper.cs
  3. 66
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  4. 355
      src/Avalonia.X11/X11TrayIconImpl.cs
  5. 2
      src/Avalonia.X11/X11Window.cs

5
src/Avalonia.Base/Logging/LogArea.cs

@ -39,5 +39,10 @@ namespace Avalonia.Logging
/// The log event comes from Win32Platform.
/// </summary>
public const string Win32Platform = nameof(Win32Platform);
/// <summary>
/// The log event comes from Win32Platform.
/// </summary>
public const string X11Platform = nameof(X11Platform);
}
}

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

66
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<NativeMenu> _menus = new HashSet<NativeMenu>();
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<IRegistrar>(
"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<IRegistrar>(
"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<IPlatformIconLoader>();
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;
}

355
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<IStatusNotifierWatcher>("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);
}
}
/// <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;
private event Action<PropertyChanges> OnPropertyChange;
public event Action OnTitleChanged;
public event Action OnIconChanged;
public event Action OnAttentionIconChanged;
public event Action OnOverlayIconChanged;
public event Action OnTooltipChanged;
public Action<INativeMenuExporter> SetNativeMenuExporter { get; set; }
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 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<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError = null)
{
OnTitleChanged += handler;
return Disposable.Create(() => OnTitleChanged -= handler);
}
public async Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError = null)
{
OnIconChanged += handler;
return Disposable.Create(() => OnIconChanged -= handler);
}
public async Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError = null)
{
OnAttentionIconChanged += handler;
return Disposable.Create(() => OnAttentionIconChanged -= handler);
}
public async Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError = null)
{
OnOverlayIconChanged += handler;
return Disposable.Create(() => OnOverlayIconChanged -= handler);
}
public async Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError = null)
{
OnTooltipChanged += handler;
return Disposable.Create(() => OnTooltipChanged -= handler);
}
public async Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError = null)
{
NewStatusAsync += handler;
return Disposable.Create(() => NewStatusAsync -= handler);
}
public async Task<object> GetAsync(string prop)
{
if (prop.Contains("Menu"))
{
return _backingProperties.Menu;
}
return default;
}
public async Task<StatusNotifierItemProperties> GetAllAsync()
{
return _backingProperties;
}
public Task SetAsync(string prop, object val) => Task.CompletedTask;
public async Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler)
{
OnPropertyChange += handler;
return Disposable.Create(() => OnPropertyChange -= handler);
}
public void SetIcon(DbusPixmap dbusPixmap)
{
_backingProperties.IconPixmap = new[] { dbusPixmap };
InvalidateAll();
}
public void SetTitleAndTooltip(string text)
{
_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 = null);
Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError = null);
Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError = null);
Task<object> GetAsync(string prop);
Task<StatusNotifierItemProperties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[Dictionary]
internal class StatusNotifierItemProperties
{
public string Category;
public string Id;
public string Title;
public string Status;
public int WindowId;
public string IconThemePath;
public ObjectPath Menu;
public bool ItemIsMenu;
public string IconName;
public DbusPixmap[] IconPixmap;
public string OverlayIconName;
public DbusPixmap[] OverlayIconPixmap;
public string AttentionIconName;
public DbusPixmap[] AttentionIconPixmap;
public string AttentionMovieName;
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, new byte[] { }), new DbusPixmap(0, 0, new 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;
}
}
}

2
src/Avalonia.X11/X11Window.cs

@ -190,7 +190,7 @@ namespace Avalonia.X11
if(_popup)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
if (platform.Options.UseDBusMenu)
NativeMenuExporter = DBusMenuExporter.TryCreate(_handle);
NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle);
NativeControlHost = new X11NativeControlHost(_platform, this);
DispatcherTimer.Run(() =>
{

Loading…
Cancel
Save