// hardcodet.net NotifyIcon for WPF // Copyright (c) 2009 - 2013 Philipp Sumi // Contact and Information: http://www.hardcodet.net // // This library is free software; you can redistribute it and/or // modify it under the terms of the Code Project Open License (CPOL); // either version 1.0 of the License, or (at your option) any later // version. // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // THIS COPYRIGHT NOTICE MAY NOT BE REMOVED FROM THIS FILE using System; using System.Drawing; using System.Linq; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using MangaReader.Avalonia.Platform.Win.Interop; namespace MangaReader.Avalonia.Platform.Win { /// /// A WPF proxy to for a taskbar icon (NotifyIcon) that sits in the system's /// taskbar notification area ("system tray"). /// public class TaskBarIcon : IDisposable { private readonly object lockObject = new object(); #region Members /// /// Represents the current icon data. /// private NotifyIconData iconData; /// /// Receives messages from the taskbar icon. /// private readonly WindowMessageSink messageSink; /// /// Indicates whether the taskbar icon has been created or not. /// public bool IsTaskbarIconCreated { get; private set; } public Icon Icon { get; } public event EventHandler MouseEventHandler; #endregion #region Construction /// /// Initializes the taskbar icon and registers a message listener /// in order to receive events from the taskbar area. /// public TaskBarIcon(Icon icon) { Icon = icon; // using dummy sink in design mode messageSink = new WindowMessageSink(NotifyIconVersion.Vista); // init icon data structure iconData = NotifyIconData.CreateDefault(messageSink.MessageWindowHandle); iconData.IconHandle = Icon?.Handle ?? IntPtr.Zero; iconData.ToolTipText = nameof(MangaReader); // create the taskbar icon CreateTaskbarIcon(); // register event listeners messageSink.MouseEventReceived += OnMouseEvent; messageSink.TaskbarCreated += OnTaskbarCreated; // register listener in order to get notified when the application closes if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { lifetime.Exit += OnExit; } } #endregion #region Process Incoming Mouse Events /// /// Processes mouse events, which are bubbled /// through the class' routed events, trigger /// certain actions (e.g. show a popup), or /// both. /// /// Event flag. private void OnMouseEvent(MouseEvent me) { if (IsDisposed) return; switch (me) { case MouseEvent.MouseMove: // immediately return - there's nothing left to evaluate return; case MouseEvent.IconRightMouseDown: case MouseEvent.IconLeftMouseDown: case MouseEvent.IconRightMouseUp: case MouseEvent.IconLeftMouseUp: case MouseEvent.IconMiddleMouseDown: case MouseEvent.IconMiddleMouseUp: case MouseEvent.BalloonToolTipClicked: case MouseEvent.IconDoubleClick: MouseEventHandler?.Invoke(this, me); break; default: throw new ArgumentOutOfRangeException(nameof(me), "Missing handler for mouse event flag: " + me); } } #endregion #region Balloon Tips /// /// Displays a balloon tip with the specified title, /// text, and icon in the taskbar for the specified time period. /// /// The title to display on the balloon tip. /// The text to display on the balloon tip. /// A symbol that indicates the severity. public void ShowBalloonTip(string title, string message, BalloonFlags symbol) { lock (lockObject) { ShowBalloonTip(title, message, symbol, IntPtr.Zero); } } /// /// Invokes in order to display /// a given balloon ToolTip. /// /// The title to display on the balloon tip. /// The text to display on the balloon tip. /// Indicates what icon to use. /// A handle to a custom icon, if any, or /// . private void ShowBalloonTip(string title, string message, BalloonFlags flags, IntPtr balloonIconHandle) { EnsureNotDisposed(); iconData.BalloonText = message ?? string.Empty; iconData.BalloonTitle = title ?? string.Empty; iconData.BalloonFlags = flags; iconData.CustomBalloonIconHandle = balloonIconHandle; WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info | IconDataMembers.Icon); } /// /// Hides a balloon ToolTip, if any is displayed. /// public void HideBalloonTip() { EnsureNotDisposed(); // reset balloon by just setting the info to an empty string iconData.BalloonText = iconData.BalloonTitle = string.Empty; WriteIconData(ref iconData, NotifyCommand.Modify, IconDataMembers.Info); } #endregion #region Create / Remove Taskbar Icon /// /// Recreates the taskbar icon if the whole taskbar was /// recreated (e.g. because Explorer was shut down). /// private void OnTaskbarCreated() { IsTaskbarIconCreated = false; CreateTaskbarIcon(); } /// /// Creates the taskbar icon. This message is invoked during initialization, /// if the taskbar is restarted, and whenever the icon is displayed. /// private void CreateTaskbarIcon() { lock (lockObject) { if (IsTaskbarIconCreated) { return; } const IconDataMembers members = IconDataMembers.Message | IconDataMembers.Icon | IconDataMembers.Tip; //write initial configuration var status = WriteIconData(ref iconData, NotifyCommand.Add, members); if (!status) { // couldn't create the icon - we can assume this is because explorer is not running (yet!) // -> try a bit later again rather than throwing an exception. Typically, if the windows // shell is being loaded later, this method is being re-invoked from OnTaskbarCreated // (we could also retry after a delay, but that's currently YAGNI) return; } messageSink.Version = (NotifyIconVersion)iconData.VersionOrTimeout; IsTaskbarIconCreated = true; } } /// /// Closes the taskbar icon if required. /// private void RemoveTaskbarIcon() { lock (lockObject) { // make sure we didn't schedule a creation if (!IsTaskbarIconCreated) { return; } WriteIconData(ref iconData, NotifyCommand.Delete, IconDataMembers.Message); IsTaskbarIconCreated = false; } } #endregion #region Dispose / Exit /// /// Set to true as soon as Dispose has been invoked. /// public bool IsDisposed { get; private set; } /// /// Checks if the object has been disposed and /// raises a in case /// the flag is true. /// private void EnsureNotDisposed() { if (IsDisposed) throw new ObjectDisposedException(GetType().FullName); } /// /// Disposes the class if the application exits. /// private void OnExit(object sender, EventArgs e) { Dispose(); } /// /// This destructor will run only if the /// method does not get called. This gives this base class the /// opportunity to finalize. /// /// Important: Do not provide destructor in types derived from this class. /// /// ~TaskBarIcon() { Dispose(false); } /// /// Disposes the object. /// /// This method is not virtual by design. Derived classes /// should override . /// public void Dispose() { Dispose(true); // This object will be cleaned up by the Dispose method. // Therefore, you should call GC.SuppressFinalize to // take this object off the finalization queue // and prevent finalization code for this object // from executing a second time. GC.SuppressFinalize(this); } /// /// Closes the tray and releases all resources. /// /// /// Dispose(bool disposing) executes in two distinct scenarios. /// If disposing equals true, the method has been called directly /// or indirectly by a user's code. Managed and unmanaged resources /// can be disposed. /// /// If disposing equals false, the method /// has been called by the runtime from inside the finalizer and you /// should not reference other objects. Only unmanaged resources can /// be disposed. /// Check the property to determine whether /// the method has already been called. private void Dispose(bool disposing) { // don't do anything if the component is already disposed if (IsDisposed || !disposing) return; lock (lockObject) { IsDisposed = true; // de-register application event listener if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) { lifetime.Exit -= OnExit; } // dispose message sink messageSink.Dispose(); // remove icon RemoveTaskbarIcon(); } } #endregion #region WriteIconData /// /// Updates the taskbar icons with data provided by a given /// instance. /// /// Configuration settings for the NotifyIcon. /// Operation on the icon (e.g. delete the icon). /// Defines which members of the /// structure are set. /// True if the data was successfully written. /// See Shell_NotifyIcon documentation on MSDN for details. private bool WriteIconData(ref NotifyIconData data, NotifyCommand command, IconDataMembers flags) { data.ValidMembers |= flags; lock (lockObject) { return WinApi.Shell_NotifyIcon(command, ref data); } } #endregion /// /// Reads a given image resource into a WinForms icon. /// /// Image source pointing to /// an icon file (*.ico). /// An icon object that can be used with the /// taskbar area. public static Icon ToIcon(string imageSource) { if (imageSource == null) return null; var executingAssembly = System.Reflection.Assembly.GetExecutingAssembly(); if (executingAssembly.GetManifestResourceNames().Contains(imageSource)) { var stream = executingAssembly.GetManifestResourceStream(imageSource); return new Icon(stream); } return null; } } }