diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Interop/WindowMessageSink.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Interop/WindowMessageSink.cs index 8b55f18..cfe363c 100644 --- a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Interop/WindowMessageSink.cs +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Interop/WindowMessageSink.cs @@ -25,7 +25,6 @@ using System; using System.ComponentModel; using System.Diagnostics; -using System.Runtime.InteropServices; namespace Hardcodet.Wpf.TaskbarNotification.Interop { @@ -290,10 +289,12 @@ namespace Hardcodet.Wpf.TaskbarNotification.Interop MouseEventReceived(MouseEvent.BalloonToolTipClicked); break; + //show ToolTip case 0x406: ChangeToolTipStateRequest(true); break; + //hide ToolTip case 0x407: ChangeToolTipStateRequest(false); break; diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/NotifyIconWpf.csproj b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/NotifyIconWpf.csproj index 981fc5e..66f7845 100644 --- a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/NotifyIconWpf.csproj +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/NotifyIconWpf.csproj @@ -53,6 +53,7 @@ + @@ -68,6 +69,7 @@ + diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupActivationMode.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupActivationMode.cs index 1415f39..36528e2 100644 --- a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupActivationMode.cs +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupActivationMode.cs @@ -70,6 +70,17 @@ namespace Hardcodet.Wpf.TaskbarNotification /// /// The item is displayed whenever a click occurs. /// + AnyClick, + + /// + /// The mouse is hovering over the Notify Icon (ToolTip behavior). + /// + Hover, + + /// + /// The item is displayed whenever a click occurs, or the mouse hovers + /// over the icon (ToolTip behavior). + /// All } } \ No newline at end of file diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupHandler.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupHandler.cs new file mode 100644 index 0000000..981791f --- /dev/null +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/PopupHandler.cs @@ -0,0 +1,344 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Threading; +using Hardcodet.Wpf.TaskbarNotification.Interop; + +namespace Hardcodet.Wpf.TaskbarNotification +{ + + public class PopupSetting : DependencyObject + { + #region Trigger + + /// + /// Trigger Dependency Property + /// + public static readonly DependencyProperty TriggerProperty = + DependencyProperty.Register("Trigger", typeof(PopupActivationMode), typeof(PopupSetting), + new FrameworkPropertyMetadata((PopupActivationMode)PopupActivationMode.AnyClick)); + + /// + /// Gets or sets the Trigger property. This dependency property + /// indicates .... + /// + public PopupActivationMode Trigger + { + get { return (PopupActivationMode)GetValue(TriggerProperty); } + set { SetValue(TriggerProperty, value); } + } + + #endregion + + + + #region PreviewOpen + + /// + /// PreviewOpen Routed Event + /// + public static readonly RoutedEvent PreviewOpenEvent = EventManager.RegisterRoutedEvent("PreviewOpen", + RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PopupSetting)); + + /// + /// Occurs when ... + /// + public event RoutedEventHandler PreviewOpen + { + add { RoutedEventHelper.AddHandler(this, PreviewOpenEvent, value); } + remove { RoutedEventHelper.RemoveHandler(this, PreviewOpenEvent, value); } + } + + /// + /// A helper method to raise the PreviewOpen event. + /// + protected RoutedEventArgs RaisePreviewOpenEvent() + { + return RaisePreviewOpenEvent(this); + } + + /// + /// A static helper method to raise the PreviewOpen event on a target element. + /// + /// UIElement or ContentElement on which to raise the event + internal static RoutedEventArgs RaisePreviewOpenEvent(DependencyObject target) + { + if (target == null) return null; + + RoutedEventArgs args = new RoutedEventArgs(); + args.RoutedEvent = PreviewOpenEvent; + RoutedEventHelper.RaiseEvent(target, args); + return args; + } + + #endregion + + } + + /// + /// Maintains a given popup, and optionally tracks activation / closing. + /// + public class PopupHandler + { + public FrameworkElement Parent { get; private set; } + public DispatcherTimer Timer { get; private set; } + + private Action scheduledTimerAction; + private Popup managedPopup; + + /// + /// Indicates whether the popup is being activated if the + /// user enters the mouse. + /// + public bool ActivateOnMouseEnterPopup { get; set; } + + /// + /// Indicates whether the popup is being deactivated if the + /// user leaves the popup. Can be set to false in order to + /// keep a popup open indefinitely once the user hovered + /// over it. Defaults to true. + /// + public bool CloseOnMouseLeavePopup { get; set; } + + public Popup ManagedPopup + { + get { return managedPopup; } + set + { + ResetSchedule(); + + var oldPopup = managedPopup; + managedPopup = value; + InitPopup(oldPopup); + } + } + + public Func PreviewOpenFunc { get; set; } + public Action PostOpenAction { get; set; } + + public Func PreviewCloseFunc { get; set; } + public Action PostCloseAction { get; set; } + + public int OpenPopupDelay { get; set; } + public int ClosePopupDelay { get; set; } + + public PopupHandler(FrameworkElement parent) + { + Parent = parent; + Timer = new DispatcherTimer(DispatcherPriority.Normal, parent.Dispatcher); + Timer.Tick += OnTimerElapsed; + CloseOnMouseLeavePopup = true; + } + + + private void InitPopup(Popup oldPopup) + { + if (oldPopup != null) + { + oldPopup.MouseEnter -= OnPopupMouseEnter; + oldPopup.MouseLeave -= OnPopupMouseLeave; + } + + if (ManagedPopup == null) return; + + ManagedPopup.MouseEnter += OnPopupMouseEnter; + ManagedPopup.MouseLeave += OnPopupMouseLeave; + + //hook up the popup with the data context of it's associated object + //in case we have content with bindings + var binding = new Binding + { + Path = new PropertyPath(FrameworkElement.DataContextProperty), + Mode = BindingMode.OneWay, + Source = Parent + }; + BindingOperations.SetBinding(ManagedPopup, FrameworkElement.DataContextProperty, binding); + + //force template application so we can switch visual states + var fe = ManagedPopup.Child as FrameworkElement; + if (fe != null) + { + fe.ApplyTemplate(); + GoToState("Closed", false); + } + } + + + public void ShowPopup() + { + Debug.WriteLine("show popup request"); + + if (ManagedPopup == null) return; + + //if the popup is still open, open it immediatly + if (ManagedPopup.IsOpen) + { + //the popup is already open (maybe was scheduled to close) + //-> abort schedule, set back to active state + ResetSchedule(); + GoToState("Showing"); + } + else + { + //validate whether we can open + bool isHandled = PreviewOpenFunc(); + if (isHandled) return; + + Schedule(OpenPopupDelay, () => + { + Debug.WriteLine("showing popup"); + + //Open the popup, then start transition + ManagedPopup.IsOpen = true; + GoToState("Showing"); + + FocusPopup(); + + PostOpenAction(); + }); + } + } + + /// + /// Schedules closing the maintained popup, if it is currently open, using + /// the configured , if set. + /// + public void ScheduleClosePopup() + { + Debug.WriteLine("close request"); + + if (ManagedPopup == null || !ManagedPopup.IsOpen) return; + + //validate whether we can close + bool isHandled = PreviewCloseFunc(); + if (isHandled) return; + + //start hiding immediately + GoToState("Hiding"); + + Schedule(ClosePopupDelay, () => + { + Debug.WriteLine("Close request scheduled"); + GoToState("Closed"); + ManagedPopup.IsOpen = false; + PostCloseAction(); + }); + } + + /// + /// Suppresses a scheduled hiding of the popup, and transitions the content + /// back into active state. + /// + private void OnPopupMouseEnter(object sender, MouseEventArgs e) + { + if (!ActivateOnMouseEnterPopup) return; + + Debug.WriteLine("popup enter"); + + //the popup is still open - just supress any scheduled action + ResetSchedule(); + GoToState("Active"); + } + + + /// + /// Schedules hiding of the control if the user moves away from an interactive popup. + /// + private void OnPopupMouseLeave(object sender, MouseEventArgs e) + { + if (!CloseOnMouseLeavePopup) return; + + Debug.WriteLine("mouse leave"); + ScheduleClosePopup(); + } + + /// + /// Performs a pending action. + /// + private void OnTimerElapsed(object sender, EventArgs eventArgs) + { + lock (Timer) + { + Timer.Stop(); + + var action = scheduledTimerAction; + scheduledTimerAction = null; + + //cache action and set timer action to null before invoking it, not the other way around + //(that action could cause the timer action to be reassigned) + if (action != null) + { + action(); + } + } + } + + private void ResetSchedule() + { + lock (Timer) + { + Timer.Stop(); + scheduledTimerAction = null; + } + } + + /// + /// Schedules an action to be executed on the next timer tick. Resets the timer + /// and replaces any other pending action. + /// + /// + /// Customize this if you need custom delays to show / hide / fade tooltips by + /// simply changing the timer interval depending on the state change. + /// + private void Schedule(int delay, Action action) + { + lock (Timer) + { + Timer.Stop(); + + if (delay == 0) + { + //if there is no delay, execute action immediately + scheduledTimerAction = null; + action(); + } + else + { + Timer.Interval = TimeSpan.FromMilliseconds(delay); + scheduledTimerAction = action; + Timer.Start(); + } + } + } + + private void GoToState(string stateName, bool useTransitions = true) + { + var fe = ManagedPopup.Child as FrameworkElement; + if (fe == null) return; + + VisualStateManager.GoToState(fe, stateName, useTransitions); + } + + private void FocusPopup() + { + IntPtr handle = IntPtr.Zero; + if (ManagedPopup.Child != null) + { + //try to get a handle on the popup itself (via its child) + HwndSource source = (HwndSource)PresentationSource.FromVisual(ManagedPopup.Child); + if (source != null) handle = source.Handle; + } + + //TODO if we don't have a handle for the popup, fall back to the message sink + //if (handle == IntPtr.Zero) handle = notifyIcon.messageSink.MessageWindowHandle; + + //activate either popup or message sink to track deactivation. + //otherwise, the popup does not close if the user clicks somewhere else + WinApi.SetForegroundWindow(handle); + } + } +} \ No newline at end of file diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.Declarations.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.Declarations.cs index a12c56e..93395d5 100644 --- a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.Declarations.cs +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.Declarations.cs @@ -96,7 +96,7 @@ namespace Hardcodet.Wpf.TaskbarNotification /// TrayToolTipResolved Read-Only Dependency Property /// private static readonly DependencyPropertyKey TrayToolTipResolvedPropertyKey - = DependencyProperty.RegisterReadOnly("TrayToolTipResolved", typeof (ToolTip), typeof (TaskbarIcon), + = DependencyProperty.RegisterReadOnly("TrayToolTipResolved", typeof(Popup), typeof(TaskbarIcon), new FrameworkPropertyMetadata(null)); @@ -116,9 +116,9 @@ namespace Hardcodet.Wpf.TaskbarNotification [Category(CategoryName)] [Browsable(true)] [Bindable(true)] - public ToolTip TrayToolTipResolved + public Popup TrayToolTipResolved { - get { return (ToolTip) GetValue(TrayToolTipResolvedProperty); } + get { return (Popup)GetValue(TrayToolTipResolvedProperty); } } /// @@ -126,7 +126,7 @@ namespace Hardcodet.Wpf.TaskbarNotification /// property. /// /// The new value for the property. - protected void SetTrayToolTipResolved(ToolTip value) + protected void SetTrayToolTipResolved(Popup value) { SetValue(TrayToolTipResolvedPropertyKey, value); } @@ -304,7 +304,7 @@ namespace Hardcodet.Wpf.TaskbarNotification //do not touch tooltips if we have a custom tooltip element if (TrayToolTip == null) { - ToolTip currentToolTip = TrayToolTipResolved; + Popup currentToolTip = TrayToolTipResolved; if (currentToolTip == null) { //if we don't have a wrapper tooltip for the tooltip text, create it now @@ -313,7 +313,7 @@ namespace Hardcodet.Wpf.TaskbarNotification else { //if we have a wrapper tooltip that shows the old tooltip text, just update content - currentToolTip.Content = e.NewValue; + currentToolTip.Child = new ContentControl {Content = e.NewValue}; //TODO hackery } } @@ -375,7 +375,7 @@ namespace Hardcodet.Wpf.TaskbarNotification /// Provides information about the updated property. private void OnTrayToolTipPropertyChanged(DependencyPropertyChangedEventArgs e) { - //recreate tooltip control + //recreate tooltip popup CreateCustomToolTip(); if (e.OldValue != null) @@ -388,6 +388,8 @@ namespace Hardcodet.Wpf.TaskbarNotification { //set this taskbar icon as a reference to the new tooltip element SetParentTaskbarIcon((DependencyObject) e.NewValue, this); + + TrayToolTip.IsHitTestVisible = IsToolTipInteractive; } //update tooltip settings - needed to make sure a string is set, even @@ -398,6 +400,37 @@ namespace Hardcodet.Wpf.TaskbarNotification #endregion + #region IsToolTipInteractive + + public static readonly DependencyProperty IsToolTipInteractiveProperty = + DependencyProperty.Register("IsToolTipInteractive", typeof(bool), typeof(TaskbarIcon), + new FrameworkPropertyMetadata(true, OnIsToolTipInteractiveChanged)); + + [Bindable(true)] + [Category(CategoryName)] + [DisplayName("Clickable ToolTip")] + [Description("Whether the ToolTip can be selected / clicked on.")] + public bool IsToolTipInteractive + { + get { return (bool)GetValue(IsToolTipInteractiveProperty); } + set { SetValue(IsToolTipInteractiveProperty, value); } + } + + /// + /// Handles changes to the IsToolTipInteractive property. + /// + private static void OnIsToolTipInteractiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var target = (TaskbarIcon)d; + + if (target.TrayToolTip != null) + { + target.TrayToolTip.IsHitTestVisible = target.IsToolTipInteractive; + } + } + + #endregion + #region TrayPopup dependency property /// diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.cs index 884eedc..d33cf5d 100644 --- a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.cs +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/TaskbarIcon.cs @@ -29,6 +29,7 @@ using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; +using System.Windows.Data; using System.Windows.Interop; using System.Windows.Threading; using Hardcodet.Wpf.TaskbarNotification.Interop; @@ -67,6 +68,11 @@ namespace Hardcodet.Wpf.TaskbarNotification /// private readonly Timer singleClickTimer; + /// + /// Maintains opened tooltip popups. + /// + private ToolTipObserver toolTipObserver; + /// /// A timer that is used to close open balloon tooltips. /// @@ -466,21 +472,23 @@ namespace Hardcodet.Wpf.TaskbarNotification private void OnToolTipChange(bool visible) { //if we don't have a tooltip, there's nothing to do here... - if (TrayToolTipResolved == null) return; + //if (TrayToolTipResolved == null) return; if (visible) { - if (IsPopupOpen) + if (IsPopupOpen) //TODO return if IsEnabled is false { //ignore if we are already displaying something down there return; } var args = RaisePreviewTrayToolTipOpenEvent(); + + //if the user handled the event by herself, we're done if (args.Handled) return; - TrayToolTipResolved.IsOpen = true; - + toolTipObserver.ShowToolTip(); + //raise attached event first if (TrayToolTip != null) RaiseToolTipOpenedEvent(TrayToolTip); @@ -493,11 +501,16 @@ namespace Hardcodet.Wpf.TaskbarNotification if (args.Handled) return; //raise attached event first - if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip); + if (TrayToolTip != null) RaiseToolTipCloseEvent(TrayToolTip); //TODO this must be fired with a delay once the observer really closed it! - TrayToolTipResolved.IsOpen = false; + //TrayToolTipResolved.IsOpen = false; + if (!toolTipObserver.IsMouseOverToolTip) + { + toolTipObserver.BeginCloseToolTip(); + } //bubble event + //TODO this must be fired with a delay once the observer really closed it! RaiseTrayToolTipCloseEvent(); } } @@ -519,44 +532,64 @@ namespace Hardcodet.Wpf.TaskbarNotification private void CreateCustomToolTip() { //check if the item itself is a tooltip - ToolTip tt = TrayToolTip as ToolTip; + var popup = TrayToolTip as Popup; - if (tt == null && TrayToolTip != null) + if (popup == null && TrayToolTip != null) { - //create an invisible wrapper tooltip that hosts the UIElement - tt = new ToolTip(); - tt.Placement = PlacementMode.Mouse; + //create an invisible popup that hosts the UIElement + popup = new Popup(); + popup.AllowsTransparency = true; + + //don't animate by default - devs can use attached + //events or override + popup.PopupAnimation = PopupAnimation.None; //TODO make this configurable + + //the CreateRootPopup method outputs binding errors in the debug window because + //it tries to bind to "Popup-specific" properties in case they are provided by the child. + //We don't need that so just assign the control as the child. + popup.Child = TrayToolTip; //do *not* set the placement target, as it causes the popup to become hidden if the //TaskbarIcon's parent is hidden, too. At runtime, the parent can be resolved through //the ParentTaskbarIcon attached dependency property: - //tt.PlacementTarget = this; + //popup.PlacementTarget = this; - //make sure the tooltip is invisible - tt.HasDropShadow = false; - tt.BorderThickness = new Thickness(0); - tt.Background = System.Windows.Media.Brushes.Transparent; - - //setting the - tt.StaysOpen = true; - tt.Content = TrayToolTip; + popup.Placement = PlacementMode.Mouse; } - else if (tt == null && !String.IsNullOrEmpty(ToolTipText)) + else if (popup == null && !String.IsNullOrEmpty(ToolTipText)) { - //create a simple tooltip for the ToolTipText string - tt = new ToolTip(); - tt.Content = ToolTipText; + //TODO create means to show a regular tooltip instead of this hackery + var toolTip = new ToolTip(); + popup = new Popup(); //TODO hack + popup.ToolTip = toolTip; + toolTip.Content = ToolTipText; + + //wire max width + var binding = new Binding + { + Path = new PropertyPath(Popup.IsOpenProperty), + Mode = BindingMode.OneWay, + Source = popup + }; + BindingOperations.SetBinding(toolTip, System.Windows.Controls.ToolTip.IsOpenProperty, binding); } //the tooltip explicitly gets the DataContext of this instance. //If there is no DataContext, the TaskbarIcon assigns itself - if (tt != null) + if (popup != null) { - UpdateDataContext(tt, null, DataContext); + UpdateDataContext(popup, null, DataContext); } //store a reference to the used tooltip - SetTrayToolTipResolved(tt); + SetTrayToolTipResolved(popup); + + if (popup != null) + { + //TODO that should just become a property setter on the observer, with null allowed + if(toolTipObserver == null) toolTipObserver = new ToolTipObserver(this, 500); + toolTipObserver.Popup = popup; + } } @@ -666,20 +699,8 @@ namespace Hardcodet.Wpf.TaskbarNotification //open popup TrayPopupResolved.IsOpen = true; - IntPtr handle = IntPtr.Zero; - if (TrayPopupResolved.Child != null) - { - //try to get a handle on the popup itself (via its child) - HwndSource source = (HwndSource) PresentationSource.FromVisual(TrayPopupResolved.Child); - if (source != null) handle = source.Handle; - } - - //if we don't have a handle for the popup, fall back to the message sink - if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle; - - //activate either popup or message sink to track deactivation. - //otherwise, the popup does not close if the user clicks somewhere else - WinApi.SetForegroundWindow(handle); + //activate element + FocusElement(TrayPopupResolved); //raise attached event - item should never be null unless developers //changed the CustomPopup directly... @@ -690,6 +711,29 @@ namespace Hardcodet.Wpf.TaskbarNotification } } + + /// + /// Activates a given UI element. + /// + private void FocusElement(Popup popup) + { + IntPtr handle = IntPtr.Zero; + if (popup.Child != null) + { + //try to get a handle on the popup itself (via its child) + HwndSource source = (HwndSource)PresentationSource.FromVisual(popup.Child); + if (source != null) handle = source.Handle; + } + + //if we don't have a handle for the popup, fall back to the message sink + if (handle == IntPtr.Zero) handle = messageSink.MessageWindowHandle; + + //activate either popup or message sink to track deactivation. + //otherwise, the popup does not close if the user clicks somewhere else + WinApi.SetForegroundWindow(handle); + + } + #endregion #region Context Menu diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/ToolTipObserver.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/ToolTipObserver.cs new file mode 100644 index 0000000..531b5c8 --- /dev/null +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/ToolTipObserver.cs @@ -0,0 +1,266 @@ +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Threading; +using Hardcodet.Wpf.TaskbarNotification.Interop; + +namespace Hardcodet.Wpf.TaskbarNotification +{ + public enum CloseMode + { + /// + /// Popup is not closed manually. + /// + None, + + OnLeavePopup, + } + + + /// + /// Maintains a currently displayed, optionally interactive "ToolTip" (which is infact a popup). + /// + internal class ToolTipObserver + { + private readonly TaskbarIcon notifyIcon; + private readonly DispatcherTimer timer; + private Action timerAction; + + public Popup Popup + { + get { return popup; } + set + { + popup = value; + InitControls(); + } + } + + /// + /// Caches whether the mouse is currently over the popup. In that case, ignore + /// the notify's request to close the popup (leads into an endless open/close cycle). + /// + private bool isMouseOverPopup; + + private Popup popup; + + public bool IsMouseOverToolTip + { + //TODO replace with better call + get { return popup != null && popup.IsMouseOver; } + } + + + + + public ToolTipObserver(TaskbarIcon notifyIcon, int delay) + { + this.notifyIcon = notifyIcon; + + timer = new DispatcherTimer(TimeSpan.FromMilliseconds(delay), DispatcherPriority.Normal, OnTimerElapsed, notifyIcon.Dispatcher); + timer.IsEnabled = false; + + //EventManager.RegisterClassHandler(type, Mouse.MouseEnterEvent, (Delegate)new MouseEventHandler(UIElement.OnMouseEnterThunk), false); + //EventManager.RegisterClassHandler(type, Mouse.MouseLeaveEvent, (Delegate)new MouseEventHandler(UIElement.OnMouseLeaveThunk), false); + } + + /// + /// Shows the popup. Typically invoked if the user hovers over the NotifyIcon. + /// + public void ShowToolTip() + { + Debug.WriteLine("show request from NI"); + + //don't do anything + if (!notifyIcon.IsEnabled) return; //TODO should move into the control, also for other operations + + //if the popup is still open, open it immediatly + if (Popup != null && Popup.IsOpen) + { + Debug.WriteLine("show request scheduled"); + Schedule(() => { }); //reset schedule + GoToState("Showing"); + } + else + { + Schedule(() => + { + Debug.WriteLine("showing popup"); + Popup.IsOpen = true; //show, then transition + GoToState("Showing"); + + IntPtr handle = IntPtr.Zero; + Popup ttPopup = notifyIcon.TrayToolTipResolved; + if (ttPopup.Child != null) + { + //try to get a handle on the popup itself (via its child) + HwndSource source = (HwndSource)PresentationSource.FromVisual(ttPopup.Child); + if (source != null) handle = source.Handle; + } + + //if we don't have a handle for the popup, fall back to the message sink + //if (handle == IntPtr.Zero) handle = notifyIcon.messageSink.MessageWindowHandle; + + //activate either popup or message sink to track deactivation. + //otherwise, the popup does not close if the user clicks somewhere else + WinApi.SetForegroundWindow(handle); + }); + } + } + + /// + /// Transitions the popup into a closed state. + /// + public void BeginCloseToolTip() + { + Debug.WriteLine("close request from notify icon"); + + + //Popup.InputHitTest(p == null) + + if (Popup.IsMouseOver) + { + Debug.WriteLine("ignoring close request since mouse is over tooltip"); + return; + } + + if (Popup == null) return; + + //start fading immediately if animation is programmed that way + GoToState("Hiding"); + + Schedule(() => + { + Debug.WriteLine("Close request scheduled"); + GoToState("Closed"); + Popup.IsOpen = false; + }); + } + + + + /// + /// Suppresses a scheduled hiding of the popup, and transitions the content + /// back into active state. + /// + private void OnPopupMouseEnter(object sender, MouseEventArgs e) + { + Debug.WriteLine("popup enter"); + + this.isMouseOverPopup = true; + + timer.Stop(); //suppress any other actions + GoToState("Active"); //the popup is still open + } + + + /// + /// Schedules hiding of the control if the user moves away from an interactive popup. + /// + private void OnPopupMouseLeave(object sender, MouseEventArgs e) + { + Debug.WriteLine("mouse leave"); + this.isMouseOverPopup = false; + + Schedule(() => + { + Debug.WriteLine("mouse leave scheduler"); + + //start hiding immediately + GoToState("Hiding"); //switch with a delay when leaving the popup + + Schedule(() => + { + Popup.IsOpen = false; + }); //close with yet another delay + }); + } + + + + /// + /// Inits the helper popup and tooltip controls. + /// + private void InitControls() + { + Popup.MouseEnter += OnPopupMouseEnter; + Popup.MouseLeave += OnPopupMouseLeave; + + //hook up the popup with the data context of it's associated object + //in case we have content with bindings + var binding = new Binding + { + Path = new PropertyPath(FrameworkElement.DataContextProperty), + Mode = BindingMode.OneWay, + Source = notifyIcon + }; + BindingOperations.SetBinding(Popup, FrameworkElement.DataContextProperty, binding); + + //force template application so we can switch visual states + var fe = notifyIcon.TrayToolTip as FrameworkElement; + if (fe != null) + { + fe.ApplyTemplate(); + GoToState("Closed", false); + } + } + + + + + /// + /// Schedules an action to be executed on the next timer tick. Resets the timer + /// and replaces any other pending action. + /// + /// + /// Customize this if you need custom delays to show / hide / fade tooltips by + /// simply changing the timer interval depending on the state change. + /// + private void Schedule(Action action) + { + lock (timer) + { + //close whatever was scheduled + timer.Stop(); + + timerAction = action; + timer.Start(); + } + } + + /// + /// Performs a pending action. + /// + private void OnTimerElapsed(object sender, EventArgs eventArgs) + { + lock (timer) + { + timer.Stop(); + + var action = timerAction; + timerAction = null; + + //cache action and set timer action to null before invoking + //(that action could cause the timeraction to be reassigned) + if (action != null) + { + action(); + } + } + } + + + private void GoToState(string stateName, bool useTransitions = true) + { + var fe = notifyIcon.TrayToolTip as FrameworkElement; + if (fe == null) return; + + VisualStateManager.GoToState(fe, stateName, useTransitions); + } + + } +} diff --git a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Util.cs b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Util.cs index b7db32a..6b9375e 100644 --- a/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Util.cs +++ b/Hardcodet.NotifyIcon.Wpf/Source/NotifyIconWpf/Util.cs @@ -25,6 +25,7 @@ using System; using System.ComponentModel; using System.Drawing; +using System.Linq; using System.Windows; using System.Windows.Input; using System.Windows.Media; @@ -202,14 +203,7 @@ namespace Hardcodet.Wpf.TaskbarNotification /// is a null reference. public static bool Is(this T value, params T[] candidates) { - if (candidates == null) return false; - - foreach (var t in candidates) - { - if (value.Equals(t)) return true; - } - - return false; + return candidates != null && candidates.Contains(value); } #endregion @@ -236,9 +230,11 @@ namespace Hardcodet.Wpf.TaskbarNotification return me.Is(MouseEvent.IconDoubleClick); case PopupActivationMode.MiddleClick: return me == MouseEvent.IconMiddleMouseUp; - case PopupActivationMode.All: + case PopupActivationMode.AnyClick: //return true for everything except mouse movements return me != MouseEvent.MouseMove; + case PopupActivationMode.All: + return true; default: throw new ArgumentOutOfRangeException("activationMode"); }