271 changed files with 7346 additions and 776 deletions
@ -0,0 +1,33 @@ |
|||
//
|
|||
// trayicon.h
|
|||
// Avalonia.Native.OSX
|
|||
//
|
|||
// Created by Dan Walmsley on 09/09/2021.
|
|||
// Copyright © 2021 Avalonia. All rights reserved.
|
|||
//
|
|||
|
|||
#ifndef trayicon_h |
|||
#define trayicon_h |
|||
|
|||
#include "common.h" |
|||
|
|||
class AvnTrayIcon : public ComSingleObject<IAvnTrayIcon, &IID_IAvnTrayIcon> |
|||
{ |
|||
private: |
|||
NSStatusItem* _native; |
|||
|
|||
public: |
|||
FORWARD_IUNKNOWN() |
|||
|
|||
AvnTrayIcon(); |
|||
|
|||
~AvnTrayIcon (); |
|||
|
|||
virtual HRESULT SetIcon (void* data, size_t length) override; |
|||
|
|||
virtual HRESULT SetMenu (IAvnMenu* menu) override; |
|||
|
|||
virtual HRESULT SetIsVisible (bool isVisible) override; |
|||
}; |
|||
|
|||
#endif /* trayicon_h */ |
|||
@ -0,0 +1,85 @@ |
|||
#include "common.h" |
|||
#include "trayicon.h" |
|||
#include "menu.h" |
|||
|
|||
extern IAvnTrayIcon* CreateTrayIcon() |
|||
{ |
|||
@autoreleasepool |
|||
{ |
|||
return new AvnTrayIcon(); |
|||
} |
|||
} |
|||
|
|||
AvnTrayIcon::AvnTrayIcon() |
|||
{ |
|||
_native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength]; |
|||
|
|||
} |
|||
|
|||
AvnTrayIcon::~AvnTrayIcon() |
|||
{ |
|||
if(_native != nullptr) |
|||
{ |
|||
[[_native statusBar] removeStatusItem:_native]; |
|||
_native = nullptr; |
|||
} |
|||
} |
|||
|
|||
HRESULT AvnTrayIcon::SetIcon (void* data, size_t length) |
|||
{ |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
if(data != nullptr) |
|||
{ |
|||
NSData *imageData = [NSData dataWithBytes:data length:length]; |
|||
NSImage *image = [[NSImage alloc] initWithData:imageData]; |
|||
|
|||
NSSize originalSize = [image size]; |
|||
|
|||
NSSize size; |
|||
size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; |
|||
|
|||
auto scaleFactor = size.height / originalSize.height; |
|||
size.width = originalSize.width * scaleFactor; |
|||
|
|||
[image setSize: size]; |
|||
[_native setImage:image]; |
|||
} |
|||
else |
|||
{ |
|||
[_native setImage:nullptr]; |
|||
} |
|||
return S_OK; |
|||
} |
|||
} |
|||
|
|||
HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu) |
|||
{ |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
auto appMenu = dynamic_cast<AvnAppMenu*>(menu); |
|||
|
|||
if(appMenu != nullptr) |
|||
{ |
|||
[_native setMenu:appMenu->GetNative()]; |
|||
} |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
|
|||
HRESULT AvnTrayIcon::SetIsVisible(bool isVisible) |
|||
{ |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
[_native setVisible:isVisible]; |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using MiniMvvm; |
|||
|
|||
namespace ControlCatalog.ViewModels |
|||
{ |
|||
public class ApplicationViewModel : ViewModelBase |
|||
{ |
|||
public ApplicationViewModel() |
|||
{ |
|||
ExitCommand = MiniCommand.Create(() => |
|||
{ |
|||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) |
|||
{ |
|||
lifetime.Shutdown(); |
|||
} |
|||
}); |
|||
|
|||
ToggleCommand = MiniCommand.Create(() => { }); |
|||
} |
|||
|
|||
public MiniCommand ExitCommand { get; } |
|||
|
|||
public MiniCommand ToggleCommand { get; } |
|||
} |
|||
} |
|||
@ -1,13 +1,9 @@ |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Metadata; |
|||
#if SIGNED_BUILD
|
|||
|
|||
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] |
|||
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] |
|||
#else
|
|||
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")] |
|||
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")] |
|||
#endif
|
|||
|
|||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] |
|||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")] |
|||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] |
|||
|
|||
@ -0,0 +1,433 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
using System.Linq; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Styling; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class MaskedTextBox : TextBox, IStyleable |
|||
{ |
|||
public static readonly StyledProperty<bool> AsciiOnlyProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly)); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture, |
|||
(o, v) => o.Culture = v, CultureInfo.CurrentCulture); |
|||
|
|||
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave)); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool?> MaskCompletedProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskCompleted), o => o.MaskCompleted); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool?> MaskFullProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskFull), o => o.MaskFull); |
|||
|
|||
public static readonly StyledProperty<string?> MaskProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty); |
|||
|
|||
public static new readonly StyledProperty<char> PasswordCharProperty = |
|||
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar), '\0'); |
|||
|
|||
public static readonly StyledProperty<char> PromptCharProperty = |
|||
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_'); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v); |
|||
|
|||
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty = |
|||
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v); |
|||
|
|||
private CultureInfo? _culture; |
|||
|
|||
private bool _resetOnPrompt = true; |
|||
|
|||
private bool _ignoreTextChanges; |
|||
|
|||
private bool _resetOnSpace = true; |
|||
|
|||
public MaskedTextBox() { } |
|||
|
|||
/// <summary>
|
|||
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
|
|||
/// </summary>
|
|||
public MaskedTextBox(MaskedTextProvider maskedTextProvider) |
|||
{ |
|||
if (maskedTextProvider == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(maskedTextProvider)); |
|||
} |
|||
AsciiOnly = maskedTextProvider.AsciiOnly; |
|||
Culture = maskedTextProvider.Culture; |
|||
Mask = maskedTextProvider.Mask; |
|||
PasswordChar = maskedTextProvider.PasswordChar; |
|||
PromptChar = maskedTextProvider.PromptChar; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters.
|
|||
/// Default value is false.
|
|||
/// </summary>
|
|||
public bool AsciiOnly |
|||
{ |
|||
get => GetValue(AsciiOnlyProperty); |
|||
set => SetValue(AsciiOnlyProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the culture information associated with the masked text box.
|
|||
/// </summary>
|
|||
public CultureInfo? Culture |
|||
{ |
|||
get => _culture; |
|||
set => SetAndRaise(CultureProperty, ref _culture, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus.
|
|||
/// </summary>
|
|||
public bool HidePromptOnLeave |
|||
{ |
|||
get => GetValue(HidePromptOnLeaveProperty); |
|||
set => SetValue(HidePromptOnLeaveProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the mask to apply to the TextBox.
|
|||
/// </summary>
|
|||
public string? Mask |
|||
{ |
|||
get => GetValue(MaskProperty); |
|||
set => SetValue(MaskProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies whether the test string required input positions, as specified by the mask, have
|
|||
/// all been assigned.
|
|||
/// </summary>
|
|||
public bool? MaskCompleted |
|||
{ |
|||
get => MaskProvider?.MaskCompleted; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specifies whether all inputs (required and optional) have been provided into the mask successfully.
|
|||
/// </summary>
|
|||
public bool? MaskFull |
|||
{ |
|||
get => MaskProvider?.MaskFull; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the MaskTextProvider for the specified Mask.
|
|||
/// </summary>
|
|||
public MaskedTextProvider? MaskProvider { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the character to be displayed in substitute for user input.
|
|||
/// </summary>
|
|||
public new char PasswordChar |
|||
{ |
|||
get => GetValue(PasswordCharProperty); |
|||
set => SetValue(PasswordCharProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
|
|||
/// </summary>
|
|||
public char PromptChar |
|||
{ |
|||
get => GetValue(PromptCharProperty); |
|||
set => SetValue(PromptCharProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed.
|
|||
/// </summary>
|
|||
public bool ResetOnPrompt |
|||
{ |
|||
get => _resetOnPrompt; |
|||
set |
|||
{ |
|||
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value); |
|||
if (MaskProvider != null) |
|||
{ |
|||
MaskProvider.ResetOnPrompt = value; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if selected characters should be reset when the space character is pressed.
|
|||
/// </summary>
|
|||
public bool ResetOnSpace |
|||
{ |
|||
get => _resetOnSpace; |
|||
set |
|||
{ |
|||
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value); |
|||
if (MaskProvider != null) |
|||
{ |
|||
MaskProvider.ResetOnSpace = value; |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
Type IStyleable.StyleKey => typeof(TextBox); |
|||
|
|||
protected override void OnGotFocus(GotFocusEventArgs e) |
|||
{ |
|||
if (HidePromptOnLeave == true && MaskProvider != null) |
|||
{ |
|||
Text = MaskProvider.ToDisplayString(); |
|||
} |
|||
base.OnGotFocus(e); |
|||
} |
|||
|
|||
protected override async void OnKeyDown(KeyEventArgs e) |
|||
{ |
|||
if (MaskProvider == null) |
|||
{ |
|||
base.OnKeyDown(e); |
|||
return; |
|||
} |
|||
|
|||
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>(); |
|||
|
|||
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e)); |
|||
|
|||
if (Match(keymap.Paste)) |
|||
{ |
|||
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync(); |
|||
|
|||
if (text == null) |
|||
return; |
|||
|
|||
foreach (var item in text) |
|||
{ |
|||
var index = GetNextCharacterPosition(CaretIndex); |
|||
if (MaskProvider.InsertAt(item, index)) |
|||
{ |
|||
CaretIndex = ++index; |
|||
} |
|||
} |
|||
|
|||
Text = MaskProvider.ToDisplayString(); |
|||
e.Handled = true; |
|||
return; |
|||
} |
|||
|
|||
if (e.Key != Key.Back) |
|||
{ |
|||
base.OnKeyDown(e); |
|||
} |
|||
|
|||
switch (e.Key) |
|||
{ |
|||
case Key.Delete: |
|||
if (CaretIndex < Text.Length) |
|||
{ |
|||
if (MaskProvider.RemoveAt(CaretIndex)) |
|||
{ |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
} |
|||
|
|||
e.Handled = true; |
|||
} |
|||
break; |
|||
case Key.Space: |
|||
if (!MaskProvider.ResetOnSpace || string.IsNullOrEmpty(SelectedText)) |
|||
{ |
|||
if (MaskProvider.InsertAt(" ", CaretIndex)) |
|||
{ |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
} |
|||
} |
|||
|
|||
e.Handled = true; |
|||
break; |
|||
case Key.Back: |
|||
if (CaretIndex > 0) |
|||
{ |
|||
MaskProvider.RemoveAt(CaretIndex - 1); |
|||
} |
|||
RefreshText(MaskProvider, CaretIndex - 1); |
|||
e.Handled = true; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
protected override void OnLostFocus(RoutedEventArgs e) |
|||
{ |
|||
if (HidePromptOnLeave == true && MaskProvider != null) |
|||
{ |
|||
Text = MaskProvider.ToString(!HidePromptOnLeave, true); |
|||
} |
|||
base.OnLostFocus(e); |
|||
} |
|||
|
|||
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
|||
{ |
|||
void UpdateMaskProvider() |
|||
{ |
|||
MaskProvider = new MaskedTextProvider(Mask, Culture, true, PromptChar, PasswordChar, AsciiOnly) { ResetOnSpace = ResetOnSpace, ResetOnPrompt = ResetOnPrompt }; |
|||
if (Text != null) |
|||
{ |
|||
MaskProvider.Set(Text); |
|||
} |
|||
RefreshText(MaskProvider, 0); |
|||
} |
|||
if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false) |
|||
{ |
|||
if (string.IsNullOrEmpty(Text)) |
|||
{ |
|||
MaskProvider.Clear(); |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
base.OnPropertyChanged(change); |
|||
return; |
|||
} |
|||
|
|||
MaskProvider.Set(Text); |
|||
RefreshText(MaskProvider, CaretIndex); |
|||
} |
|||
else if (change.Property == MaskProperty) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
|
|||
if (!string.IsNullOrEmpty(Mask)) |
|||
{ |
|||
foreach (var c in Mask!) |
|||
{ |
|||
if (!MaskedTextProvider.IsValidMaskChar(c)) |
|||
{ |
|||
throw new ArgumentException("Specified mask contains characters that are not valid."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else if (change.Property == PasswordCharProperty) |
|||
{ |
|||
if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar)) |
|||
{ |
|||
throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar)); |
|||
} |
|||
if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar) |
|||
{ |
|||
// Prompt and password chars must be different.
|
|||
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); |
|||
} |
|||
if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
} |
|||
} |
|||
else if (change.Property == PromptCharProperty) |
|||
{ |
|||
if (!MaskedTextProvider.IsValidInputChar(PromptChar)) |
|||
{ |
|||
throw new ArgumentException("Specified character value is not allowed for this property."); |
|||
} |
|||
if (PromptChar == PasswordChar) |
|||
{ |
|||
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same."); |
|||
} |
|||
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
} |
|||
} |
|||
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly |
|||
|| change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture)) |
|||
{ |
|||
UpdateMaskProvider(); |
|||
} |
|||
base.OnPropertyChanged(change); |
|||
} |
|||
protected override void OnTextInput(TextInputEventArgs e) |
|||
{ |
|||
_ignoreTextChanges = true; |
|||
try |
|||
{ |
|||
if (IsReadOnly) |
|||
{ |
|||
e.Handled = true; |
|||
base.OnTextInput(e); |
|||
return; |
|||
} |
|||
if (MaskProvider == null) |
|||
{ |
|||
base.OnTextInput(e); |
|||
return; |
|||
} |
|||
if ((MaskProvider.ResetOnSpace && e.Text == " " || MaskProvider.ResetOnPrompt && e.Text == MaskProvider.PromptChar.ToString()) && !string.IsNullOrEmpty(SelectedText)) |
|||
{ |
|||
if (SelectionStart > SelectionEnd ? MaskProvider.RemoveAt(SelectionEnd, SelectionStart - 1) : MaskProvider.RemoveAt(SelectionStart, SelectionEnd - 1)) |
|||
{ |
|||
SelectedText = string.Empty; |
|||
} |
|||
} |
|||
|
|||
if (CaretIndex < Text.Length) |
|||
{ |
|||
CaretIndex = GetNextCharacterPosition(CaretIndex); |
|||
|
|||
if (MaskProvider.InsertAt(e.Text, CaretIndex)) |
|||
{ |
|||
CaretIndex++; |
|||
} |
|||
var nextPos = GetNextCharacterPosition(CaretIndex); |
|||
if (nextPos != 0 && CaretIndex != Text.Length) |
|||
{ |
|||
CaretIndex = nextPos; |
|||
} |
|||
} |
|||
|
|||
RefreshText(MaskProvider, CaretIndex); |
|||
|
|||
|
|||
e.Handled = true; |
|||
|
|||
base.OnTextInput(e); |
|||
} |
|||
finally |
|||
{ |
|||
_ignoreTextChanges = false; |
|||
} |
|||
|
|||
} |
|||
|
|||
private int GetNextCharacterPosition(int startPosition) |
|||
{ |
|||
if (MaskProvider != null) |
|||
{ |
|||
var position = MaskProvider.FindEditPositionFrom(startPosition, true); |
|||
if (CaretIndex != -1) |
|||
{ |
|||
return position; |
|||
} |
|||
} |
|||
return startPosition; |
|||
} |
|||
|
|||
private void RefreshText(MaskedTextProvider provider, int position) |
|||
{ |
|||
if (provider != null) |
|||
{ |
|||
Text = provider.ToDisplayString(); |
|||
CaretIndex = position; |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
using Avalonia.Controls.Platform; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
public interface ITrayIconImpl : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Sets the icon of this tray icon.
|
|||
/// </summary>
|
|||
void SetIcon(IWindowIconImpl? icon); |
|||
|
|||
/// <summary>
|
|||
/// Sets the icon of this tray icon.
|
|||
/// </summary>
|
|||
void SetToolTipText(string? text); |
|||
|
|||
/// <summary>
|
|||
/// Sets if the tray icon is visible or not.
|
|||
/// </summary>
|
|||
void SetIsVisible(bool visible); |
|||
|
|||
/// <summary>
|
|||
/// Gets the MenuExporter to allow native menus to be exported to the TrayIcon.
|
|||
/// </summary>
|
|||
INativeMenuExporter? MenuExporter { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or Sets the Action that is called when the TrayIcon is clicked.
|
|||
/// </summary>
|
|||
Action? OnClicked { get; set; } |
|||
} |
|||
} |
|||
@ -1,8 +1,13 @@ |
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Platform |
|||
{ |
|||
public interface IWindowingPlatform |
|||
{ |
|||
IWindowImpl CreateWindow(); |
|||
|
|||
IWindowImpl CreateEmbeddableWindow(); |
|||
|
|||
ITrayIconImpl? CreateTrayIcon(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,20 @@ |
|||
namespace Avalonia.Controls.Templates |
|||
{ |
|||
public class TemplateResult<T> |
|||
{ |
|||
public T Result { get; } |
|||
public INameScope NameScope { get; } |
|||
|
|||
public TemplateResult(T result, INameScope nameScope) |
|||
{ |
|||
Result = result; |
|||
NameScope = nameScope; |
|||
} |
|||
|
|||
public void Deconstruct(out T result, out INameScope scope) |
|||
{ |
|||
result = Result; |
|||
scope = NameScope; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,234 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Windows.Input; |
|||
using Avalonia.Collections; |
|||
using Avalonia.Controls.ApplicationLifetimes; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Utilities; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public sealed class TrayIcons : AvaloniaList<TrayIcon> |
|||
{ |
|||
} |
|||
|
|||
|
|||
|
|||
public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable |
|||
{ |
|||
private readonly ITrayIconImpl? _impl; |
|||
private ICommand? _command; |
|||
|
|||
private TrayIcon(ITrayIconImpl? impl) |
|||
{ |
|||
if (impl != null) |
|||
{ |
|||
_impl = impl; |
|||
|
|||
_impl.SetIsVisible(IsVisible); |
|||
|
|||
_impl.OnClicked = () => |
|||
{ |
|||
Clicked?.Invoke(this, EventArgs.Empty); |
|||
|
|||
if (Command?.CanExecute(CommandParameter) == true) |
|||
{ |
|||
Command.Execute(CommandParameter); |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
|
|||
public TrayIcon() : this(PlatformManager.CreateTrayIcon()) |
|||
{ |
|||
} |
|||
|
|||
static TrayIcon() |
|||
{ |
|||
IconsProperty.Changed.Subscribe(args => |
|||
{ |
|||
if (args.Sender is Application) |
|||
{ |
|||
if (args.OldValue.Value != null) |
|||
{ |
|||
RemoveIcons(args.OldValue.Value); |
|||
} |
|||
|
|||
if (args.NewValue.Value != null) |
|||
{ |
|||
args.NewValue.Value.CollectionChanged += Icons_CollectionChanged; |
|||
} |
|||
} |
|||
}); |
|||
|
|||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) |
|||
{ |
|||
lifetime.Exit += Lifetime_Exit; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Raised when the TrayIcon is clicked.
|
|||
/// Note, this is only supported on Win32 and some Linux DEs,
|
|||
/// on OSX this event is not raised.
|
|||
/// </summary>
|
|||
public event EventHandler? Clicked; |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Command"/> property.
|
|||
/// </summary>
|
|||
public static readonly DirectProperty<TrayIcon, ICommand?> CommandProperty = |
|||
Button.CommandProperty.AddOwner<TrayIcon>( |
|||
trayIcon => trayIcon.Command, |
|||
(trayIcon, command) => trayIcon.Command = command, |
|||
enableDataValidation: true); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="CommandParameter"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<object?> CommandParameterProperty = |
|||
Button.CommandParameterProperty.AddOwner<MenuItem>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="TrayIcons"/> attached property.
|
|||
/// </summary>
|
|||
public static readonly AttachedProperty<TrayIcons> IconsProperty |
|||
= AvaloniaProperty.RegisterAttached<TrayIcon, Application, TrayIcons>("Icons"); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Menu"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<NativeMenu?> MenuProperty |
|||
= AvaloniaProperty.Register<TrayIcon, NativeMenu?>(nameof(Menu)); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="Icon"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<WindowIcon> IconProperty = |
|||
Window.IconProperty.AddOwner<TrayIcon>(); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ToolTipText"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<string?> ToolTipTextProperty = |
|||
AvaloniaProperty.Register<TrayIcon, string?>(nameof(ToolTipText)); |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="IsVisible"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> IsVisibleProperty = |
|||
Visual.IsVisibleProperty.AddOwner<TrayIcon>(); |
|||
|
|||
public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons); |
|||
|
|||
public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="Command"/> property of a TrayIcon.
|
|||
/// </summary>
|
|||
public ICommand? Command |
|||
{ |
|||
get => _command; |
|||
set => SetAndRaise(CommandProperty, ref _command, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
|
|||
/// <see cref="TrayIcon"/>.
|
|||
/// </summary>
|
|||
public object CommandParameter |
|||
{ |
|||
get { return GetValue(CommandParameterProperty); } |
|||
set { SetValue(CommandParameterProperty, value); } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the Menu of the TrayIcon.
|
|||
/// </summary>
|
|||
public NativeMenu? Menu |
|||
{ |
|||
get => GetValue(MenuProperty); |
|||
set => SetValue(MenuProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the icon of the TrayIcon.
|
|||
/// </summary>
|
|||
public WindowIcon Icon |
|||
{ |
|||
get => GetValue(IconProperty); |
|||
set => SetValue(IconProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the tooltip text of the TrayIcon.
|
|||
/// </summary>
|
|||
public string? ToolTipText |
|||
{ |
|||
get => GetValue(ToolTipTextProperty); |
|||
set => SetValue(ToolTipTextProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the visibility of the TrayIcon.
|
|||
/// </summary>
|
|||
public bool IsVisible |
|||
{ |
|||
get => GetValue(IsVisibleProperty); |
|||
set => SetValue(IsVisibleProperty, value); |
|||
} |
|||
|
|||
public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter; |
|||
|
|||
private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e) |
|||
{ |
|||
var trayIcons = GetIcons(Application.Current); |
|||
|
|||
RemoveIcons(trayIcons); |
|||
} |
|||
|
|||
private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
RemoveIcons(e.OldItems.Cast<TrayIcon>()); |
|||
} |
|||
|
|||
private static void RemoveIcons(IEnumerable<TrayIcon> icons) |
|||
{ |
|||
foreach (var icon in icons) |
|||
{ |
|||
icon.Dispose(); |
|||
} |
|||
} |
|||
|
|||
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
|||
{ |
|||
base.OnPropertyChanged(change); |
|||
|
|||
if (change.Property == IconProperty) |
|||
{ |
|||
_impl?.SetIcon(Icon.PlatformImpl); |
|||
} |
|||
else if (change.Property == IsVisibleProperty) |
|||
{ |
|||
_impl?.SetIsVisible(change.NewValue.GetValueOrDefault<bool>()); |
|||
} |
|||
else if (change.Property == ToolTipTextProperty) |
|||
{ |
|||
_impl?.SetToolTipText(change.NewValue.GetValueOrDefault<string?>()); |
|||
} |
|||
else if (change.Property == MenuProperty) |
|||
{ |
|||
_impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault<NativeMenu>()); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Disposes the tray icon (removing it from the tray area).
|
|||
/// </summary>
|
|||
public void Dispose() => _impl?.Dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,457 @@ |
|||
#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)] |
|||
|
|||
[assembly: |
|||
InternalsVisibleTo( |
|||
"Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] |
|||
|
|||
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 }); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue