Browse Source

initial implementation of tray icon.

pull/6610/head
Dan Walmsley 4 years ago
parent
commit
3438ac149b
  1. 9
      samples/ControlCatalog/App.xaml.cs
  2. 3
      src/Avalonia.Controls/ApiCompatBaseline.txt
  3. 22
      src/Avalonia.Controls/Platform/ITrayIconImpl.cs
  4. 3
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  5. 13
      src/Avalonia.Controls/Platform/PlatformManager.cs
  6. 107
      src/Avalonia.Controls/TrayIcon.cs
  7. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  8. 24
      src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs
  9. 5
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  10. 5
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  11. 6
      src/Avalonia.X11/X11Platform.cs
  12. 60
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  13. 252
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  14. 5
      src/Windows/Avalonia.Win32/Win32Platform.cs

9
samples/ControlCatalog/App.xaml.cs

@ -1,5 +1,6 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
@ -92,12 +93,20 @@ namespace ControlCatalog
Styles.Insert(0, FluentLight);
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
var trayIcon = new TrayIcon();
trayIcon.Icon = desktopLifetime.MainWindow.Icon;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
singleViewLifetime.MainView = new MainView();

3
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -55,4 +55,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
Total Issues: 56
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 57

22
src/Avalonia.Controls/Platform/ITrayIconImpl.cs

@ -0,0 +1,22 @@
using System;
namespace Avalonia.Platform
{
public interface ITrayIconImpl
{
/// <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);
}
}

3
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@ -3,6 +3,9 @@ namespace Avalonia.Platform
public interface IWindowingPlatform
{
IWindowImpl CreateWindow();
IWindowImpl CreateEmbeddableWindow();
ITrayIconImpl CreateTrayIcon();
}
}

13
src/Avalonia.Controls/Platform/PlatformManager.cs

@ -22,6 +22,19 @@ namespace Avalonia.Controls.Platform
{
}
public static ITrayIconImpl CreateTrayIcon ()
{
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
if (platform == null)
{
throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered.");
}
return s_designerMode ? null : platform.CreateTrayIcon();
}
public static IWindowImpl CreateWindow()
{
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();

107
src/Avalonia.Controls/TrayIcon.cs

@ -0,0 +1,107 @@
using System;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Controls
{
public class TrayIcon : AvaloniaObject, IDataContextProvider
{
private readonly ITrayIconImpl _impl;
private TrayIcon(ITrayIconImpl impl)
{
_impl = impl;
}
public TrayIcon () : this(PlatformManager.CreateTrayIcon())
{
}
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public static readonly StyledProperty<object?> DataContextProperty =
StyledElement.DataContextProperty.AddOwner<Application>();
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
public static readonly StyledProperty<WindowIcon> IconProperty =
Window.IconProperty.AddOwner<TrayIcon>();
public static readonly StyledProperty<string?> ToolTipTextProperty =
AvaloniaProperty.Register<TrayIcon, string?>(nameof(ToolTipText));
/// <summary>
/// Defines the <see cref="IsVisibleProperty"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVisibleProperty =
Visual.IsVisibleProperty.AddOwner<TrayIcon>();
/// <summary>
/// Removes the notify icon from the taskbar notification area.
/// </summary>
public void Remove()
{
}
public new ITrayIconImpl PlatformImpl => _impl;
/// <summary>
/// Gets or sets the Applications's data context.
/// </summary>
/// <remarks>
/// The data context property specifies the default object that will
/// be used for data binding.
/// </remarks>
public object? DataContext
{
get => GetValue(DataContextProperty);
set => SetValue(DataContextProperty, 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);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if(change.Property == IconProperty)
{
_impl.SetIcon(Icon.PlatformImpl);
}
}
}
}

4
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote
private static DetachableTransportConnection s_lastWindowTransport;
private static PreviewerWindowImpl s_lastWindow;
public static List<object> PreFlightMessages = new List<object>();
public ITrayIconImpl CreateTrayIcon() => new TrayIconStub();
public IWindowImpl CreateWindow() => new WindowStub();
public IWindowImpl CreateEmbeddableWindow()

24
src/Avalonia.DesignerSupport/Remote/TrayIconStub.cs

@ -0,0 +1,24 @@
using System;
using Avalonia.Platform;
namespace Avalonia.DesignerSupport.Remote
{
class TrayIconStub : ITrayIconImpl
{
public Action Clicked { get; set; }
public Action DoubleClicked { get; set; }
public Action RightClicked { get; set; }
public void SetIcon(IWindowIconImpl icon)
{
}
public void SetIsVisible(bool visible)
{
}
public void SetToolTipText(string text)
{
}
}
}

5
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -51,6 +51,11 @@ namespace Avalonia.Headless
public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
public ITrayIconImpl CreateTrayIcon()
{
throw new NotImplementedException();
}
}
internal static void Initialize()

5
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -134,6 +134,11 @@ namespace Avalonia.Native
}
}
public ITrayIconImpl CreateTrayIcon ()
{
throw new NotImplementedException();
}
public IWindowImpl CreateWindow()
{
return new WindowImpl(_factory, _options, _platformGl);

6
src/Avalonia.X11/X11Platform.cs

@ -100,6 +100,12 @@ namespace Avalonia.X11
public IntPtr DeferredDisplay { get; set; }
public IntPtr Display { get; set; }
public ITrayIconImpl CreateTrayIcon ()
{
throw new NotImplementedException();
}
public IWindowImpl CreateWindow()
{
return new X11Window(this, null);

60
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1172,6 +1172,9 @@ namespace Avalonia.Win32.Interop
GCW_ATOM = -32
}
[DllImport("shell32", CharSet = CharSet.Auto)]
public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData);
[DllImport("user32.dll", EntryPoint = "SetClassLongPtr")]
private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong);
@ -2271,4 +2274,61 @@ namespace Avalonia.Win32.Interop
public uint VisibleMask;
public uint DamageMask;
}
public enum NIM : uint
{
ADD = 0x00000000,
MODIFY = 0x00000001,
DELETE = 0x00000002,
SETFOCUS = 0x00000003,
SETVERSION = 0x00000004
}
[Flags]
public enum NIF : uint
{
MESSAGE = 0x00000001,
ICON = 0x00000002,
TIP = 0x00000004,
STATE = 0x00000008,
INFO = 0x00000010,
GUID = 0x00000020,
REALTIME = 0x00000040,
SHOWTIP = 0x00000080
}
[Flags]
public enum NIIF : uint
{
NONE = 0x00000000,
INFO = 0x00000001,
WARNING = 0x00000002,
ERROR = 0x00000003,
USER = 0x00000004,
ICON_MASK = 0x0000000F,
NOSOUND = 0x00000010,
LARGE_ICON = 0x00000020,
RESPECT_QUIET_TIME = 0x00000080
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class NOTIFYICONDATA
{
public int cbSize = Marshal.SizeOf<NOTIFYICONDATA>();
public IntPtr hWnd;
public int uID;
public NIF uFlags;
public int uCallbackMessage;
public IntPtr hIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szTip;
public int dwState = 0;
public int dwStateMask = 0;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szInfo;
public int uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public NIIF dwInfoFlags;
}
}

252
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Platform;
using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32
{
/// <summary>
/// Custom Win32 window messages for the NotifyIcon
/// </summary>
public enum CustomWindowsMessage : uint
{
WM_TRAYICON = (uint)WindowsMessage.WM_APP + 1024,
WM_TRAYMOUSE = (uint)WindowsMessage.WM_USER + 1024
}
public class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup
{
public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling);
private readonly MoveResizeDelegate _moveResize;
private Window _hiddenWindow;
public TrayIconManagedPopupPositionerPopupImplHelper(MoveResizeDelegate moveResize)
{
_moveResize = moveResize;
_hiddenWindow = new Window();
}
public IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens =>
_hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo(
s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList();
public Rect ParentClientAreaScreenGeometry
{
get
{
var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft;
var size = _hiddenWindow.Screens.Primary.Bounds.Size;
return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity);
}
}
public void MoveAndResize(Point devicePoint, Size virtualSize)
{
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity);
}
public virtual double Scaling => _hiddenWindow.Screens.Primary.PixelDensity;
}
public class TrayPopupRoot : Window
{
private ManagedPopupPositioner _positioner;
public TrayPopupRoot()
{
_positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize));
Topmost = true;
LostFocus += TrayPopupRoot_LostFocus;
}
private void TrayPopupRoot_LostFocus(object sender, Interactivity.RoutedEventArgs e)
{
Close();
}
private void MoveResize(PixelPoint position, Size size, double scaling)
{
PlatformImpl.Move(position);
PlatformImpl.Resize(size, PlatformResizeReason.Layout);
}
protected override void ArrangeCore(Rect finalRect)
{
base.ArrangeCore(finalRect);
_positioner.Update(new PopupPositionerParameters
{
Anchor = PopupAnchor.TopLeft,
Gravity = PopupGravity.BottomRight,
AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)),
Size = finalRect.Size,
ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY | PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY,
});
}
}
public class TrayIconImpl : ITrayIconImpl
{
private readonly int _uniqueId = 0;
private static int _nextUniqueId = 0;
private WndProc _wndProcDelegate;
private IntPtr _hwnd;
private bool _iconAdded;
private IconImpl _icon;
public TrayIconImpl()
{
_uniqueId = ++_nextUniqueId;
CreateMessageWindow();
UpdateIcon();
}
~TrayIconImpl()
{
UpdateIcon(false);
}
private void CreateMessageWindow()
{
// Ensure that the delegate doesn't get garbage collected by storing it as a field.
_wndProcDelegate = new UnmanagedMethods.WndProc(WndProc);
UnmanagedMethods.WNDCLASSEX wndClassEx = new UnmanagedMethods.WNDCLASSEX
{
cbSize = Marshal.SizeOf<UnmanagedMethods.WNDCLASSEX>(),
lpfnWndProc = _wndProcDelegate,
hInstance = UnmanagedMethods.GetModuleHandle(null),
lpszClassName = "AvaloniaMessageWindow " + Guid.NewGuid(),
};
ushort atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx);
if (atom == 0)
{
throw new Win32Exception();
}
_hwnd = UnmanagedMethods.CreateWindowEx(0, atom, null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (_hwnd == IntPtr.Zero)
{
throw new Win32Exception();
}
}
public void SetIcon(IWindowIconImpl icon)
{
_icon = icon as IconImpl;
UpdateIcon();
}
public void SetIsVisible(bool visible)
{
if (visible)
{
}
}
public void SetToolTipText(string text)
{
throw new NotImplementedException();
}
private void UpdateIcon(bool remove = false)
{
var iconData = new NOTIFYICONDATA()
{
hWnd = _hwnd,
uID = _uniqueId,
uFlags = NIF.TIP | NIF.MESSAGE,
uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE,
hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon,
szTip = "Tool tip text here."
};
if (!remove)
{
iconData.uFlags |= NIF.ICON;
if (!_iconAdded)
{
UnmanagedMethods.Shell_NotifyIcon(NIM.ADD, iconData);
_iconAdded = true;
}
else
{
UnmanagedMethods.Shell_NotifyIcon(NIM.MODIFY, iconData);
}
}
else
{
UnmanagedMethods.Shell_NotifyIcon(NIM.DELETE, iconData);
_iconAdded = false;
}
}
private void OnRightClicked()
{
UnmanagedMethods.GetCursorPos(out UnmanagedMethods.POINT pt);
var cursor = new PixelPoint(pt.X, pt.Y);
var trayMenu = new TrayPopupRoot()
{
Position = cursor,
SystemDecorations = SystemDecorations.None,
SizeToContent = SizeToContent.WidthAndHeight,
Background = null,
TransparencyLevelHint = WindowTransparencyLevel.Transparent,
Content = new MenuFlyoutPresenter()
{
Items = new List<MenuItem>
{
new MenuItem { Header = "Item 1"},
new MenuItem { Header = "Item 2"},
new MenuItem { Header = "Item 3"},
new MenuItem { Header = "Item 4"},
new MenuItem { Header = "Item 5"}
}
}
};
trayMenu.Show();
}
private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE)
{
// Determine the type of message and call the matching event handlers
switch (lParam.ToInt32())
{
case (int)WindowsMessage.WM_LBUTTONUP:
break;
case (int)WindowsMessage.WM_LBUTTONDBLCLK:
break;
case (int)WindowsMessage.WM_RBUTTONUP:
OnRightClicked();
break;
default:
break;
}
}
return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
}
}
}

5
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -293,6 +293,11 @@ namespace Avalonia.Win32
}
}
public ITrayIconImpl CreateTrayIcon ()
{
return new TrayIconImpl();
}
public IWindowImpl CreateWindow()
{
return new WindowImpl();

Loading…
Cancel
Save