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