From 45c8867a785107c8217b2d3d4db7d1a5186a9c23 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 18 Oct 2021 23:11:49 +0300 Subject: [PATCH 01/54] Bump SDK to 5.0.302 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index b2b2da7c4f..55a59d3ab5 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.401" + "version": "5.0.302" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", From 4a2eb925f56497bfceffdb4c05de2102fbc5518d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 18 Oct 2021 23:21:46 +0300 Subject: [PATCH 02/54] Download 3.1 runtime for *nix --- build.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sh b/build.sh index bd162fab9b..9c957612cf 100755 --- a/build.sh +++ b/build.sh @@ -65,6 +65,8 @@ else else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.408 --runtime dotnet + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.408 --runtime aspnetcore fi export PATH=$DOTNET_DIRECTORY:$PATH From 2f0eb86e7121c57e64243cf4a0251ffc35d8ba79 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 18 Oct 2021 23:25:51 +0300 Subject: [PATCH 03/54] Use 3.1.20 --- build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 9c957612cf..7703e90a78 100755 --- a/build.sh +++ b/build.sh @@ -65,8 +65,8 @@ else else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.408 --runtime dotnet - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.408 --runtime aspnetcore + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.20 --runtime dotnet + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.20 --runtime aspnetcore fi export PATH=$DOTNET_DIRECTORY:$PATH From e078aa253daf625df49a5f4c494c9fb33ab3e50a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 18 Oct 2021 23:51:24 +0300 Subject: [PATCH 04/54] Download SDK separately (unix) --- .gitignore | 1 + azure-pipelines.yml | 32 ++++++++++++----------------- build.sh | 50 ++------------------------------------------- get-sdk.sh | 22 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 67 deletions(-) create mode 100755 get-sdk.sh diff --git a/.gitignore b/.gitignore index abf7674560..1f91a11604 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,4 @@ obj-Skia/ coc-settings.json .ccls-cache .ccls +sdk diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 11ef36d43f..bc60f84107 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,19 +4,18 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: CmdLine@2 - displayName: 'Install Nuke' + displayName: 'Download SDK' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 + ./get-sdk.sh - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run Build' inputs: script: | - export PATH="$PATH:$HOME/.dotnet/tools" + export PATH="`pwd`/sdk:$PATH" dotnet --info printenv - nuke --target CiAzureLinux --configuration=Release - + ./build.sh --target CiAzureLinux --configuration=Release - task: PublishTestResults@2 inputs: testResultsFormat: 'VSTest' @@ -29,11 +28,11 @@ jobs: pool: vmImage: 'macOS-10.15' steps: - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.401' + - task: CmdLine@2 + displayName: 'Download SDK' inputs: - version: 3.1.401 - + script: | + ./get-sdk.sh - task: CmdLine@2 displayName: 'Install Mono 5.18' inputs: @@ -45,6 +44,7 @@ jobs: displayName: 'Generate avalonia-native' inputs: script: | + export PATH="`pwd`/sdk:$PATH" cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h - task: Xcode@5 @@ -58,13 +58,7 @@ jobs: args: '-derivedDataPath ./' - task: CmdLine@2 - displayName: 'Install Nuke' - inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - - - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run Build' inputs: script: | export COREHOST_TRACE=0 @@ -72,10 +66,10 @@ jobs: export DOTNET_CLI_TELEMETRY_OPTOUT=1 which dotnet dotnet --info - export PATH="$PATH:$HOME/.dotnet/tools" + export PATH="`pwd`/sdk:$PATH" dotnet --info printenv - nuke --target CiAzureOSX --configuration Release --skip-previewer + ./build.sh --target CiAzureOSX --configuration Release --skip-previewer - task: PublishTestResults@2 inputs: diff --git a/build.sh b/build.sh index 7703e90a78..9532b4fbe0 100755 --- a/build.sh +++ b/build.sh @@ -20,57 +20,11 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" -TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" - -DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" -DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" -DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 export NUGET_XMLDOC_MODE="skip" -########################################################################### -# EXECUTION -########################################################################### - -function FirstJsonValue { - perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} -} - -# If global.json exists, load expected version -if [ -f "$DOTNET_GLOBAL_FILE" ]; then - DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) - if [ "$DOTNET_VERSION" == "" ]; then - unset DOTNET_VERSION - fi -fi - -# If dotnet is installed locally, and expected version is not set or installation matches the expected version -if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") || "$SKIP_DOTNET_DOWNLOAD" == "1" ]]; then - export DOTNET_EXE="$(command -v dotnet)" -else - DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" - export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" - - # Download install script - DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" - mkdir -p "$TEMP_DIRECTORY" - curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" - chmod +x "$DOTNET_INSTALL_FILE" - - # Install by channel or version - if [ -z ${DOTNET_VERSION+x} ]; then - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path - else - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path - fi - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.20 --runtime dotnet - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version 3.1.20 --runtime aspnetcore -fi - -export PATH=$DOTNET_DIRECTORY:$PATH - -echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" +dotnet --info -"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} +dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/get-sdk.sh b/get-sdk.sh new file mode 100755 index 0000000000..4427948864 --- /dev/null +++ b/get-sdk.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +SDK_DIR=$SCRIPT_DIR/sdk + +DOTNET_INSTALL_FILE="$SDK_DIR/dotnet-install.sh" +DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" + +DOTNET_VERSION=5.0.302 + +mkdir -p "$SDK_DIR" + +if [ ! -f "$DOTNET_INSTALL_FILE" ]; then + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" +fi + +"$DOTNET_INSTALL_FILE" --install-dir "$SDK_DIR" --version 5.0.302 +"$DOTNET_INSTALL_FILE" --install-dir "$SDK_DIR" --version 3.1.20 --runtime dotnet +"$DOTNET_INSTALL_FILE" --install-dir "$SDK_DIR" --version 3.1.20 --runtime aspnetcore + + From 141e749226c95557f4b44347525abbe3fa2db230 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:04:09 +0800 Subject: [PATCH 05/54] Initial Commit for handling DBus SNI Tray Icons gracefully and also making a skeleton class for the future XEmbed Tray Icon impl. --- .../DbusSNITrayIconImpl.cs | 358 +++++++++++++++++ src/Avalonia.X11/X11TrayIconImpl.cs | 379 ++++-------------- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 36 ++ 3 files changed, 462 insertions(+), 311 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs create mode 100644 src/Avalonia.X11/XEmbedTrayIconImpl.cs diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs new file mode 100644 index 0000000000..1fb74f132a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -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( + "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); + } + } + + /// + /// 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; + public event Action? OnTitleChanged; + public event Action? OnIconChanged; + public event Action? OnAttentionIconChanged; + public event Action? OnOverlayIconChanged; + public event Action? OnTooltipChanged; + 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 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 WatchNewTitleAsync(Action handler, Action onError) + { + OnTitleChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); + } + + public Task WatchNewIconAsync(Action handler, Action onError) + { + OnIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); + } + + public Task WatchNewAttentionIconAsync(Action handler, Action onError) + { + OnAttentionIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); + } + + public Task WatchNewOverlayIconAsync(Action handler, Action onError) + { + OnOverlayIconChanged += handler; + return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); + } + + public Task WatchNewToolTipAsync(Action handler, Action onError) + { + OnTooltipChanged += handler; + return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); + } + + public Task WatchNewStatusAsync(Action handler, Action onError) + { + NewStatusAsync += handler; + return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); + } + + public Task GetAsync(string prop) => Task.FromResult(new object()); + + public Task GetAllAsync() => Task.FromResult(_backingProperties); + + public Task SetAsync(string prop, object val) => Task.CompletedTask; + + public Task WatchPropertiesAsync(Action 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 WatchNewTitleAsync(Action handler, Action onError); + Task WatchNewIconAsync(Action handler, Action onError); + Task WatchNewAttentionIconAsync(Action handler, Action onError); + Task WatchNewOverlayIconAsync(Action handler, Action onError); + Task WatchNewToolTipAsync(Action handler, Action onError); + Task WatchNewStatusAsync(Action handler, Action onError); + Task GetAsync(string prop); + Task GetAllAsync(); + Task SetAsync(string prop, object val); + Task WatchPropertiesAsync(Action 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()), new DbusPixmap(0, 0, Array.Empty()) + }; + + 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; + } + } +} diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 371ff75408..ca8ed8ec35 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -1,367 +1,124 @@ -#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.X11 { 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() { - _connection = DBusHelper.TryGetConnection(); + _xEmbedTrayIcon = new XEmbedTrayIconImpl(); + + var _connection = DBusHelper.TryGetConnection(); if (_connection is null) { Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) ?.Log(this, "Unable to get a dbus connection for system tray icons."); - return; } - _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; - MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - CreateTrayIcon(); - _ctorFinished = true; + _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); } - public async void CreateTrayIcon() - { - if (_connection is null) - return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "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); + private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; - } - - public async void DestroyTrayIcon() - { - if (_connection is null) - return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; - } + private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; + private bool _isDisposed; public void Dispose() { + _dbusSniTrayIcon?.Dispose(); + _xEmbedTrayIcon?.Dispose(); _isDisposed = true; - DestroyTrayIcon(); - _connection?.Dispose(); } public void SetIcon(IWindowIconImpl? icon) { - if (_isDisposed) - return; - if (!(icon is X11IconData x11icon)) - return; + if (_isDisposed) return; - var w = (int)x11icon.Data[0]; - var h = (int)x11icon.Data[1]; - - var pixLength = w * h; - var pixByteArrayCounter = 0; - var pixByteArray = new byte[w * h * 4]; + if (_dbusSniTrayIcon?.IsActive ?? false) + { + if (!(icon is X11IconData x11icon)) + return; - for (var i = 0; i < pixLength; i++) + _dbusSniTrayIcon.SetIcon(x11icon.Data); + } + else { - var rawPixel = x11icon.Data[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); + _xEmbedTrayIcon.SetIcon(icon); } - - _icon = new DbusPixmap(w, h, pixByteArray); - _statusNotifierItemDbusObj?.SetIcon(_icon); } - public void SetIsVisible(bool visible) + public void SetToolTipText(string? text) { - if (_isDisposed || !_ctorFinished) - return; + if (_isDisposed) return; - if (visible & !_isActive) + if (_dbusSniTrayIcon?.IsActive ?? false) { - DestroyTrayIcon(); - CreateTrayIcon(); + _dbusSniTrayIcon.SetToolTipText(text); } - else if (!visible & _isActive) + else { - DestroyTrayIcon(); + _xEmbedTrayIcon.SetToolTipText(text); } } - public void SetToolTipText(string? text) - { - if (_isDisposed || text is null) - 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; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) + public void SetIsVisible(bool visible) { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + if (_isDisposed) return; - _backingProperties = new StatusNotifierItemProperties + if (_dbusSniTrayIcon?.IsActive ?? false) { - 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 WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } - - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } - - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); - } - - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } - - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) - { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); - } - - public Task GetAsync(string prop) => Task.FromResult(new object()); - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action 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 WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action 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()), new DbusPixmap(0, 0, Array.Empty()) - }; - - public ToolTip(string message) : this("", s_blank, message, "") - { + _dbusSniTrayIcon.SetIsVisible(visible); + } + else + { + _xEmbedTrayIcon.SetIsVisible(visible); + } } - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + public INativeMenuExporter? MenuExporter { - First = first; - Second = second; - Third = third; - Fourth = fourth; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + 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; - Height = height; - Data = data; + get + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + return _dbusSniTrayIcon.OnClicked; + } + else + { + return _xEmbedTrayIcon.OnClicked; + } + } + set + { + if (_dbusSniTrayIcon?.IsActive ?? false) + { + _dbusSniTrayIcon.OnClicked = value; + } + else + { + _xEmbedTrayIcon.OnClicked = value; + } + } } } } diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs new file mode 100644 index 0000000000..4b5f0d0a57 --- /dev/null +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -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; } + } +} From c4b0b99027491c78988a81559244eddc99151e4c Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Fri, 22 Oct 2021 15:05:35 +0800 Subject: [PATCH 06/54] Gracefully handle tray service restarts --- .../DbusSNITrayIconImpl.cs | 129 +++++++++++++----- src/Avalonia.X11/X11TrayIconImpl.cs | 11 +- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 16 ++- 3 files changed, 113 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs index 1fb74f132a..6ca05efe50 100644 --- a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs @@ -6,49 +6,55 @@ 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 + public class DbusSNITrayIconImpl { - private static int s_trayIconInstanceId; + private static int s_trayIconInstanceId = 0; 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; + private bool _serviceConnected; + private readonly IDisposable _serviceWatchDisposable; + private bool _isVisible; public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - public bool IsActive => _isActive; - - public DbusSNITrayIconImpl(Connection connection) + public bool IsActive => _serviceConnected; + + public DbusSNITrayIconImpl() { - _connection = connection; + _connection = DBusHelper.TryGetConnection(); + + if (_connection is null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) + ?.Log(this, "Unable to get a dbus connection for system tray icons."); + + return; + } + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); + InitializeSNWService(); CreateTrayIcon(); - _ctorFinished = true; + _serviceWatchDisposable = Watch(); } - public async void CreateTrayIcon() + private void InitializeSNWService() { - if (_connection is null) - return; + if (_connection is null || _isDisposed) return; try { @@ -61,38 +67,83 @@ namespace Avalonia.FreeDesktop 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."); + + return; } - if (_statusNotifierWatcher is null) + _serviceConnected = true; + } + + + private async Task Watch() => + await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; + + + private void OnNameChange(ServiceOwnerChangedEventArgs obj) + { + if (_isDisposed) return; + if (!_serviceConnected & obj.NewOwner != null) + { + _serviceConnected = true; + + if (_isVisible) + { + DestroyTrayIcon(); + CreateTrayIcon(); + } + else + { + DestroyTrayIcon(); + } + } + else if (_serviceConnected & obj.NewOwner is null) + { + s_trayIconInstanceId = 0; + _serviceConnected = false; + } + } + + public 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); - await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - - await _connection.RegisterServiceAsync(_sysTrayServiceName); - await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + try + { + _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); + _connection.RegisterServiceAsync(_sysTrayServiceName); + _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); + } + catch (Exception e) + { + _serviceConnected = false; + } _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - - _isActive = true; + _isVisible = true; } - public async void DestroyTrayIcon() + public void DestroyTrayIcon() { - if (_connection is null) + if (_connection is null || !_serviceConnected || _isDisposed) return; + _connection.UnregisterObject(_statusNotifierItemDbusObj); - await _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isActive = false; + _connection.UnregisterServiceAsync(_sysTrayServiceName); + _isVisible = false; } public void Dispose() @@ -100,12 +151,13 @@ namespace Avalonia.FreeDesktop _isDisposed = true; DestroyTrayIcon(); _connection?.Dispose(); + _serviceWatchDisposable?.Dispose(); } public void SetIcon(UIntPtr[] x11iconData) { if (_isDisposed) - return; + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -128,15 +180,15 @@ namespace Avalonia.FreeDesktop public void SetIsVisible(bool visible) { - if (_isDisposed || !_ctorFinished) + if (_isDisposed) return; - if (visible & !_isActive) + if (visible && !_isVisible) { DestroyTrayIcon(); CreateTrayIcon(); } - else if (!visible & _isActive) + else if (!visible && _isVisible) { DestroyTrayIcon(); } @@ -239,7 +291,20 @@ namespace Avalonia.FreeDesktop return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); } - public Task GetAsync(string prop) => Task.FromResult(new object()); + public async Task GetAsync(string prop) + { + return 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 GetAllAsync() => Task.FromResult(_backingProperties); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index ca8ed8ec35..9e03dcd604 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -11,16 +11,7 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - - var _connection = DBusHelper.TryGetConnection(); - - if (_connection is null) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform) - ?.Log(this, "Unable to get a dbus connection for system tray icons."); - } - - _dbusSniTrayIcon = new DbusSNITrayIconImpl(_connection); + _dbusSniTrayIcon = new DbusSNITrayIconImpl(); } private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index 4b5f0d0a57..c2247565be 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -9,25 +9,39 @@ namespace Avalonia.X11 { public XEmbedTrayIconImpl() { + } + + private bool IsCalled; + + private void NotImplemented() + { + if(IsCalled) return; + 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."); - } + IsCalled = true; + } + public void Dispose() { + NotImplemented(); } public void SetIcon(IWindowIconImpl? icon) { + NotImplemented(); } public void SetToolTipText(string? text) { + NotImplemented(); } public void SetIsVisible(bool visible) { + NotImplemented(); } public INativeMenuExporter? MenuExporter { get; } From 1302d3459a8863c347452b4e5fd1bbb704cb3ea4 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:13:17 +0800 Subject: [PATCH 07/54] Fix review comments --- src/Avalonia.FreeDesktop/DBusHelper.cs | 4 +- ...SNITrayIconImpl.cs => DBusTrayIconImpl.cs} | 44 +++++++++---------- src/Avalonia.X11/X11TrayIconImpl.cs | 30 ++++++------- 3 files changed, 39 insertions(+), 39 deletions(-) rename src/Avalonia.FreeDesktop/{DbusSNITrayIconImpl.cs => DBusTrayIconImpl.cs} (96%) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index 4e23711ed4..b5bfb8b116 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -51,10 +51,10 @@ namespace Avalonia.FreeDesktop public static Connection TryInitialize(string dbusAddress = null) { - return Connection ?? TryGetConnection(dbusAddress); + return Connection ?? TryCreateNewConnection(dbusAddress); } - public static Connection TryGetConnection(string dbusAddress = null) + public static Connection TryCreateNewConnection(string dbusAddress = null) { var oldContext = SynchronizationContext.Current; try diff --git a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs similarity index 96% rename from src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs rename to src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 6ca05efe50..2d30d2ee21 100644 --- a/src/Avalonia.FreeDesktop/DbusSNITrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -10,32 +10,34 @@ using Avalonia.Logging; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] - +[assembly: InternalsVisibleTo("Avalonia.X11")] namespace Avalonia.FreeDesktop { - public class DbusSNITrayIconImpl + internal class DBusTrayIconImpl { - private static int s_trayIconInstanceId = 0; + private static int s_trayIconInstanceId; + private readonly ObjectPath _dbusMenuPath; - private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private readonly Connection? _connection; - private DbusPixmap _icon; + private readonly IDisposable _serviceWatchDisposable; + + private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; private IStatusNotifierWatcher? _statusNotifierWatcher; + private DbusPixmap _icon; + private string? _sysTrayServiceName; private string? _tooltipText; private bool _isDisposed; private bool _serviceConnected; - private readonly IDisposable _serviceWatchDisposable; - private bool _isVisible; - + private bool _isVisible = true; + + public bool IsActive => _serviceConnected; public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } - - public bool IsActive => _serviceConnected; - - public DbusSNITrayIconImpl() + + public DBusTrayIconImpl() { - _connection = DBusHelper.TryGetConnection(); + _connection = DBusHelper.TryCreateNewConnection(); if (_connection is null) { @@ -46,9 +48,9 @@ namespace Avalonia.FreeDesktop } _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; + MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); - InitializeSNWService(); - CreateTrayIcon(); + _serviceWatchDisposable = Watch(); } @@ -73,12 +75,10 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; } - - + private async Task Watch() => await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; - - + private void OnNameChange(ServiceOwnerChangedEventArgs obj) { if (_isDisposed) @@ -87,6 +87,7 @@ namespace Avalonia.FreeDesktop if (!_serviceConnected & obj.NewOwner != null) { _serviceConnected = true; + InitializeSNWService(); if (_isVisible) { @@ -104,8 +105,7 @@ namespace Avalonia.FreeDesktop _serviceConnected = false; } } - - public void CreateTrayIcon() + private void CreateTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) return; @@ -136,7 +136,7 @@ namespace Avalonia.FreeDesktop _isVisible = true; } - public void DestroyTrayIcon() + private void DestroyTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) return; diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 9e03dcd604..3051e90457 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -11,17 +11,17 @@ namespace Avalonia.X11 public X11TrayIconImpl() { _xEmbedTrayIcon = new XEmbedTrayIconImpl(); - _dbusSniTrayIcon = new DbusSNITrayIconImpl(); + _dBusTrayIcon = new DBusTrayIconImpl(); } - private readonly DbusSNITrayIconImpl _dbusSniTrayIcon; + private readonly DBusTrayIconImpl _dBusTrayIcon; private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; private bool _isDisposed; public void Dispose() { - _dbusSniTrayIcon?.Dispose(); + _dBusTrayIcon?.Dispose(); _xEmbedTrayIcon?.Dispose(); _isDisposed = true; } @@ -30,12 +30,12 @@ namespace Avalonia.X11 { if (_isDisposed) return; - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { if (!(icon is X11IconData x11icon)) return; - _dbusSniTrayIcon.SetIcon(x11icon.Data); + _dBusTrayIcon.SetIcon(x11icon.Data); } else { @@ -47,9 +47,9 @@ namespace Avalonia.X11 { if (_isDisposed) return; - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - _dbusSniTrayIcon.SetToolTipText(text); + _dBusTrayIcon.SetToolTipText(text); } else { @@ -61,9 +61,9 @@ namespace Avalonia.X11 { if (_isDisposed) return; - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - _dbusSniTrayIcon.SetIsVisible(visible); + _dBusTrayIcon.SetIsVisible(visible); } else { @@ -75,9 +75,9 @@ namespace Avalonia.X11 { get { - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - return _dbusSniTrayIcon.MenuExporter; + return _dBusTrayIcon.MenuExporter; } else { @@ -90,9 +90,9 @@ namespace Avalonia.X11 { get { - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - return _dbusSniTrayIcon.OnClicked; + return _dBusTrayIcon.OnClicked; } else { @@ -101,9 +101,9 @@ namespace Avalonia.X11 } set { - if (_dbusSniTrayIcon?.IsActive ?? false) + if (_dBusTrayIcon?.IsActive ?? false) { - _dbusSniTrayIcon.OnClicked = value; + _dBusTrayIcon.OnClicked = value; } else { From be94e300807ea1849f730d8e90c7c52c6aa7dcf4 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:15:09 +0800 Subject: [PATCH 08/54] simplify logic --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 2d30d2ee21..f1146ebdd9 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -89,19 +89,15 @@ namespace Avalonia.FreeDesktop _serviceConnected = true; InitializeSNWService(); + DestroyTrayIcon(); + if (_isVisible) { - DestroyTrayIcon(); CreateTrayIcon(); } - else - { - DestroyTrayIcon(); - } } else if (_serviceConnected & obj.NewOwner is null) { - s_trayIconInstanceId = 0; _serviceConnected = false; } } @@ -110,14 +106,12 @@ namespace Avalonia.FreeDesktop 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); @@ -360,11 +354,11 @@ namespace Avalonia.FreeDesktop Task WatchPropertiesAsync(Action 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. + [Dictionary] internal class StatusNotifierItemProperties { public string? Category; From 1a9558a28a7946a496cdf2d089f7a8cc91c29d8b Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:15:37 +0800 Subject: [PATCH 09/54] fix naming --- src/Avalonia.X11/XEmbedTrayIconImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.X11/XEmbedTrayIconImpl.cs b/src/Avalonia.X11/XEmbedTrayIconImpl.cs index c2247565be..3355182a60 100644 --- a/src/Avalonia.X11/XEmbedTrayIconImpl.cs +++ b/src/Avalonia.X11/XEmbedTrayIconImpl.cs @@ -11,17 +11,17 @@ namespace Avalonia.X11 { } - private bool IsCalled; + private bool _isCalled; private void NotImplemented() { - if(IsCalled) return; + if(_isCalled) return; 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."); - IsCalled = true; + _isCalled = true; } public void Dispose() From 3f348288b35fdda7cf150db10150a06357dad248 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:43:18 +0800 Subject: [PATCH 10/54] add signed build InternalsVisibleTo --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index f1146ebdd9..4252b9f26a 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -10,7 +10,13 @@ using Avalonia.Logging; using Tmds.DBus; [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] + +#if SIGNED_BUILD +[assembly: InternalsVisibleTo("Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +#else [assembly: InternalsVisibleTo("Avalonia.X11")] +#endif + namespace Avalonia.FreeDesktop { internal class DBusTrayIconImpl From 6f16f8b66ffdcfb50cbe94905da208a7c35de018 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 14:50:18 +0800 Subject: [PATCH 11/54] some fixes --- src/Avalonia.FreeDesktop/DBusHelper.cs | 2 +- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 2 +- src/Avalonia.X11/X11TrayIconImpl.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index b5bfb8b116..c14539d7bf 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop /// This class uses synchronous execution at DBus connection establishment stage /// then switches to using AvaloniaSynchronizationContext /// - class DBusSyncContext : SynchronizationContext + private class DBusSyncContext : SynchronizationContext { private SynchronizationContext _ctx; private object _lock = new object(); diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 4252b9f26a..32033cb0a6 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -138,7 +138,7 @@ namespace Avalonia.FreeDesktop private void DestroyTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) return; _connection.UnregisterObject(_statusNotifierItemDbusObj); diff --git a/src/Avalonia.X11/X11TrayIconImpl.cs b/src/Avalonia.X11/X11TrayIconImpl.cs index 3051e90457..93bf71b409 100644 --- a/src/Avalonia.X11/X11TrayIconImpl.cs +++ b/src/Avalonia.X11/X11TrayIconImpl.cs @@ -15,7 +15,6 @@ namespace Avalonia.X11 } private readonly DBusTrayIconImpl _dBusTrayIcon; - private readonly XEmbedTrayIconImpl _xEmbedTrayIcon; private bool _isDisposed; From ef231e68361f60ea8941e55f5300fb71b74327da Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 16:35:09 +0800 Subject: [PATCH 12/54] fix --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 32033cb0a6..9534d3a32f 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -37,7 +37,7 @@ namespace Avalonia.FreeDesktop private bool _serviceConnected; private bool _isVisible = true; - public bool IsActive => _serviceConnected; + public bool IsActive { get; private set; } public INativeMenuExporter? MenuExporter { get; } public Action? OnClicked { get; set; } @@ -53,11 +53,15 @@ namespace Avalonia.FreeDesktop return; } + IsActive = true; + _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); _serviceWatchDisposable = Watch(); + + } private void InitializeSNWService() @@ -148,6 +152,7 @@ namespace Avalonia.FreeDesktop public void Dispose() { + IsActive = false; _isDisposed = true; DestroyTrayIcon(); _connection?.Dispose(); From 5871218737677086c19e51c38ee5eec5ac800cc6 Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Sun, 24 Oct 2021 16:48:18 +0800 Subject: [PATCH 13/54] fix some more --- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 9534d3a32f..1ea5d720ce 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -108,6 +108,7 @@ namespace Avalonia.FreeDesktop } else if (_serviceConnected & obj.NewOwner is null) { + DestroyTrayIcon(); _serviceConnected = false; } } @@ -137,7 +138,6 @@ namespace Avalonia.FreeDesktop _statusNotifierItemDbusObj.SetIcon(_icon); _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; - _isVisible = true; } private void DestroyTrayIcon() @@ -147,7 +147,6 @@ namespace Avalonia.FreeDesktop _connection.UnregisterObject(_statusNotifierItemDbusObj); _connection.UnregisterServiceAsync(_sysTrayServiceName); - _isVisible = false; } public void Dispose() @@ -197,6 +196,8 @@ namespace Avalonia.FreeDesktop { DestroyTrayIcon(); } + + _isVisible = visible; } public void SetToolTipText(string? text) From b7bb635c80cfedd2951b5daced726b9174ef4e12 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 24 Oct 2021 13:24:37 -0400 Subject: [PATCH 14/54] Add missing background to the presenters and popup roots --- src/Avalonia.Themes.Default/FlyoutPresenter.xaml | 2 +- src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml | 2 +- src/Avalonia.Themes.Default/PopupRoot.xaml | 1 + src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/FlyoutPresenter.xaml b/src/Avalonia.Themes.Default/FlyoutPresenter.xaml index 95f48e6c29..3bc5e085ff 100644 --- a/src/Avalonia.Themes.Default/FlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Default/FlyoutPresenter.xaml @@ -2,7 +2,7 @@ diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index ef6574c142..92344fb130 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Selector="PopupRoot"> + diff --git a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml index c0c56f2ff3..5d9d04d218 100644 --- a/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml @@ -6,16 +6,13 @@ - - - - - - + + + From 069f299b8a78e3bcadd52bb5bfc18429ff7fe4a0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 24 Oct 2021 13:25:17 -0400 Subject: [PATCH 16/54] Remove duplicated CornerRadius --- src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index 3a45c85c70..b1d3df9ddd 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -8,15 +8,6 @@ namespace Avalonia.Controls { public class MenuFlyoutPresenter : MenuBase { - public static readonly StyledProperty CornerRadiusProperty = - Border.CornerRadiusProperty.AddOwner(); - - public CornerRadius CornerRadius - { - get => GetValue(CornerRadiusProperty); - set => SetValue(CornerRadiusProperty, value); - } - public MenuFlyoutPresenter() :base(new DefaultMenuInteractionHandler(true)) { From 3bf4acce30a5270fe954c987ff4055567e45bacd Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 24 Oct 2021 13:32:13 -0400 Subject: [PATCH 17/54] Add missing background to the popup controls --- src/Avalonia.Themes.Default/AutoCompleteBox.xaml | 3 ++- src/Avalonia.Themes.Default/ComboBox.xaml | 3 ++- src/Avalonia.Themes.Default/ContextMenu.xaml | 1 + src/Avalonia.Themes.Default/MenuItem.xaml | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index fe4cd48e72..fac8ca51f8 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -21,7 +21,8 @@ MaxHeight="{TemplateBinding MaxDropDownHeight}" PlacementTarget="{TemplateBinding}" IsLightDismissEnabled="True"> - - diff --git a/src/Avalonia.Themes.Default/ContextMenu.xaml b/src/Avalonia.Themes.Default/ContextMenu.xaml index 0df4866184..987b72aaa2 100644 --- a/src/Avalonia.Themes.Default/ContextMenu.xaml +++ b/src/Avalonia.Themes.Default/ContextMenu.xaml @@ -1,4 +1,5 @@