Browse Source

initial implementation of tray icon.

Dan Walmsley 4 years ago
parent
commit
3438ac149b

+ 9 - 0
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();
 

+ 2 - 1
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 - 0
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 - 0
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

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

+ 13 - 0
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 - 0
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);
+            }
+        }
+    }
+}

+ 3 - 1
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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 - 0
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();