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");
}