mirror of
https://gitee.com/akwkevin/aistudio.-wpf.-diagram
synced 2026-03-20 00:16:36 +08:00
453 lines
15 KiB
C#
453 lines
15 KiB
C#
// ReSharper disable once CheckNamespace
|
|
namespace Fluent
|
|
{
|
|
using System;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Controls.Primitives;
|
|
using System.Windows.Input;
|
|
using Fluent.Extensions;
|
|
using Fluent.Internal;
|
|
|
|
/// <summary>
|
|
/// Dismiss popup mode.
|
|
/// </summary>
|
|
public enum DismissPopupMode
|
|
{
|
|
/// <summary>
|
|
/// Always dismiss popup.
|
|
/// </summary>
|
|
Always,
|
|
|
|
/// <summary>
|
|
/// Dismiss only if mouse is not over popup.
|
|
/// </summary>
|
|
MouseNotOver
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reason for dismiss popup event.
|
|
/// </summary>
|
|
public enum DismissPopupReason
|
|
{
|
|
/// <summary>
|
|
/// No reason given.
|
|
/// </summary>
|
|
Undefined,
|
|
|
|
/// <summary>
|
|
/// Application lost focus.
|
|
/// </summary>
|
|
ApplicationLostFocus,
|
|
|
|
/// <summary>
|
|
/// Showing key tips.
|
|
/// </summary>
|
|
ShowingKeyTips
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dismiss popup arguments.
|
|
/// </summary>
|
|
public class DismissPopupEventArgs : RoutedEventArgs
|
|
{
|
|
/// <summary>
|
|
/// Standard constructor.
|
|
/// </summary>
|
|
public DismissPopupEventArgs()
|
|
: this(DismissPopupMode.Always)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor.
|
|
/// </summary>
|
|
/// <param name="dismissMode">Dismiss mode.</param>
|
|
public DismissPopupEventArgs(DismissPopupMode dismissMode)
|
|
: this(dismissMode, DismissPopupReason.Undefined)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor.
|
|
/// </summary>
|
|
/// <param name="dismissMode">Dismiss mode.</param>
|
|
/// <param name="reason">Dismiss reason.</param>
|
|
public DismissPopupEventArgs(DismissPopupMode dismissMode, DismissPopupReason reason)
|
|
{
|
|
this.RoutedEvent = PopupService.DismissPopupEvent;
|
|
this.DismissMode = dismissMode;
|
|
this.DismissReason = reason;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Popup dismiss mode.
|
|
/// </summary>
|
|
public DismissPopupMode DismissMode { get; }
|
|
|
|
/// <summary>
|
|
/// Popup dismiss reason.
|
|
/// </summary>
|
|
public DismissPopupReason DismissReason { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
|
|
{
|
|
var handler = (EventHandler<DismissPopupEventArgs>)genericHandler;
|
|
handler(genericTarget, this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represent additional popup functionality
|
|
/// </summary>
|
|
public static class PopupService
|
|
{
|
|
#region DismissPopup
|
|
|
|
/// <summary>
|
|
/// Occurs then popup is dismissed
|
|
/// </summary>
|
|
public static readonly RoutedEvent DismissPopupEvent = EventManager.RegisterRoutedEvent("DismissPopup", RoutingStrategy.Bubble, typeof(EventHandler<DismissPopupEventArgs>), typeof(PopupService));
|
|
|
|
/// <summary>
|
|
/// Raises DismissPopup event (Async)
|
|
/// </summary>
|
|
public static void RaiseDismissPopupEventAsync(object sender, DismissPopupMode mode, DismissPopupReason reason = DismissPopupReason.Undefined)
|
|
{
|
|
var element = sender as UIElement;
|
|
|
|
if (element is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteDebug($"Dismissing Popup async (Mode = {mode}, Sender = {sender})");
|
|
|
|
element.RunInDispatcherAsync(() => RaiseDismissPopupEvent(sender, mode, reason));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises DismissPopup event
|
|
/// </summary>
|
|
public static void RaiseDismissPopupEvent(object sender, DismissPopupMode mode, DismissPopupReason reason = DismissPopupReason.Undefined)
|
|
{
|
|
var element = sender as UIElement;
|
|
|
|
if (element is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
WriteDebug($"Dismissing Popup (Mode = {mode}, Sender = {sender})");
|
|
|
|
element.RaiseEvent(new DismissPopupEventArgs(mode, reason));
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Set needed parameters to control
|
|
/// </summary>
|
|
/// <param name="classType">Control type</param>
|
|
public static void Attach(Type classType)
|
|
{
|
|
EventManager.RegisterClassHandler(classType, Mouse.PreviewMouseDownOutsideCapturedElementEvent, new MouseButtonEventHandler(OnClickThroughThunk));
|
|
EventManager.RegisterClassHandler(classType, DismissPopupEvent, new EventHandler<DismissPopupEventArgs>(OnDismissPopup));
|
|
EventManager.RegisterClassHandler(classType, FrameworkElement.ContextMenuOpeningEvent, new ContextMenuEventHandler(OnContextMenuOpening), true);
|
|
EventManager.RegisterClassHandler(classType, FrameworkElement.ContextMenuClosingEvent, new ContextMenuEventHandler(OnContextMenuClosing), true);
|
|
EventManager.RegisterClassHandler(classType, UIElement.LostMouseCaptureEvent, new MouseEventHandler(OnLostMouseCapture));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles PreviewMouseDownOutsideCapturedElementEvent event
|
|
/// </summary>
|
|
public static void OnClickThroughThunk(object sender, MouseButtonEventArgs e)
|
|
{
|
|
WriteDebug(nameof(OnClickThroughThunk));
|
|
WriteDebug($"Sender - {sender}");
|
|
WriteDebug($"OriginalSource - {e.OriginalSource}");
|
|
WriteDebug($"Mouse.Captured - {Mouse.Captured}");
|
|
|
|
if (e.ChangedButton == MouseButton.Left
|
|
|| e.ChangedButton == MouseButton.Right)
|
|
{
|
|
if (Mouse.Captured == sender
|
|
// Special handling for unknown Popups (for example datepickers used in the ribbon)
|
|
|| (sender is IDropDownControl && IsPopupRoot(Mouse.Captured)))
|
|
{
|
|
if (sender is RibbonTabControl ribbonTabControl
|
|
&& ribbonTabControl.IsMinimized
|
|
// this is true if, for example, a DatePicker popup is open and we click outside of the ribbon popup
|
|
// this should then only close the DatePicker popup but not the ribbon popup
|
|
&& IsPopupRoot(e.OriginalSource) == false)
|
|
{
|
|
// Don't close the ribbon popup if the mouse is over the ribbon popup
|
|
if (IsMousePhysicallyOver(ribbonTabControl.SelectedContentPresenter) == false)
|
|
{
|
|
// Force dismissing the Ribbon-Popup.
|
|
// Always is needed because of eager-closing-prevention.
|
|
RaiseDismissPopupEvent(sender, DismissPopupMode.Always);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RaiseDismissPopupEvent(sender, DismissPopupMode.MouseNotOver);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles lost mouse capture event
|
|
/// </summary>
|
|
public static void OnLostMouseCapture(object sender, MouseEventArgs e)
|
|
{
|
|
WriteDebug(nameof(OnLostMouseCapture));
|
|
WriteDebug($"Sender - {sender}");
|
|
WriteDebug($"OriginalSource - {e.OriginalSource}");
|
|
WriteDebug($"Mouse.Captured - {Mouse.Captured}");
|
|
|
|
var control = sender as IDropDownControl;
|
|
|
|
if (control is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Mouse.Captured == sender
|
|
|| control.IsDropDownOpen == false
|
|
|| control.IsContextMenuOpened)
|
|
{
|
|
WriteDebug($"OnLostMouseCapture => Taking no action");
|
|
return;
|
|
}
|
|
|
|
var popup = control.DropDownPopup;
|
|
|
|
if (popup?.Child is null)
|
|
{
|
|
RaiseDismissPopupEvent(sender, DismissPopupMode.MouseNotOver);
|
|
return;
|
|
}
|
|
|
|
if (e.OriginalSource == sender)
|
|
{
|
|
// If Ribbon loses capture because something outside popup is clicked - close the popup
|
|
if (popup.PlacementTarget is RibbonTabItem)
|
|
{
|
|
if (Mouse.Captured is null
|
|
|| IsAncestorOf(popup, Mouse.Captured as DependencyObject) == false)
|
|
{
|
|
RaiseDismissPopupEvent(sender, DismissPopupMode.Always);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (IsAncestorOf(popup, sender as DependencyObject) == false
|
|
&& IsAncestorOf(sender as DependencyObject, popup) == false
|
|
&& IsAncestorOf(popup, e.OriginalSource as DependencyObject) == false)
|
|
{
|
|
RaiseDismissPopupEvent(sender, DismissPopupMode.MouseNotOver);
|
|
return;
|
|
}
|
|
|
|
// This code is needed to keep some popus open.
|
|
// One of these is the ribbon popup when it's minimized.
|
|
if (e.OriginalSource != null
|
|
&& Mouse.Captured is null
|
|
&& (IsPopupRoot(e.OriginalSource) || IsAncestorOf(popup.Child, e.OriginalSource as DependencyObject)))
|
|
{
|
|
WriteDebug($"Setting mouse capture to: {sender}");
|
|
Mouse.Capture(sender as IInputElement, CaptureMode.SubTree);
|
|
e.Handled = true;
|
|
|
|
// Only raise a popup dismiss event if the source is MenuBase.
|
|
// this is because MenuBase "steals" the mouse focus in a way we have to work around here.
|
|
if (e.OriginalSource is MenuBase)
|
|
{
|
|
RaiseDismissPopupEvent(sender, DismissPopupMode.MouseNotOver);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true whether parent is ancestor of element
|
|
/// </summary>
|
|
/// <param name="parent">Parent</param>
|
|
/// <param name="element">Element</param>
|
|
/// <returns>Returns true whether parent is ancestor of element</returns>
|
|
public static bool IsAncestorOf(DependencyObject parent, DependencyObject element)
|
|
{
|
|
if (parent is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
while (element != null)
|
|
{
|
|
if (ReferenceEquals(element, parent))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
element = UIHelper.GetVisualOrLogicalParent(element);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles dismiss popup event
|
|
/// </summary>
|
|
public static void OnDismissPopup(object sender, DismissPopupEventArgs e)
|
|
{
|
|
var control = sender as IDropDownControl;
|
|
|
|
if (control is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (e.DismissMode)
|
|
{
|
|
case DismissPopupMode.Always:
|
|
DismisPopupForAlways(control, e);
|
|
break;
|
|
|
|
case DismissPopupMode.MouseNotOver:
|
|
DismisPopupForMouseNotOver(control, e);
|
|
break;
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(e.DismissMode), e.DismissMode, "Unknown DismissMode.");
|
|
}
|
|
}
|
|
|
|
private static void DismisPopupForAlways(IDropDownControl control, DismissPopupEventArgs e)
|
|
{
|
|
control.IsDropDownOpen = false;
|
|
}
|
|
|
|
private static void DismisPopupForMouseNotOver(IDropDownControl control, DismissPopupEventArgs e)
|
|
{
|
|
if (control.IsDropDownOpen == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Prevent eager closing of the Ribbon-Popup and forward mouse focus to the ribbon popup instead.
|
|
if (control is RibbonTabControl ribbonTabControl
|
|
&& ribbonTabControl.IsMinimized
|
|
&& IsAncestorOf(control as DependencyObject, e.OriginalSource as DependencyObject))
|
|
{
|
|
// Don't prevent closing if the new target is an ApplicationMenu (#581)
|
|
if (Mouse.Captured is ApplicationMenu)
|
|
{
|
|
control.IsDropDownOpen = false;
|
|
return;
|
|
}
|
|
|
|
Mouse.Capture(control as IInputElement, CaptureMode.SubTree);
|
|
return;
|
|
}
|
|
|
|
if (IsMousePhysicallyOver(control.DropDownPopup) == false)
|
|
{
|
|
control.IsDropDownOpen = false;
|
|
}
|
|
else
|
|
{
|
|
if (Mouse.Captured != control)
|
|
{
|
|
Mouse.Capture(control as IInputElement, CaptureMode.SubTree);
|
|
}
|
|
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true whether mouse is physically over the popup
|
|
/// </summary>
|
|
/// <param name="popup">Element</param>
|
|
/// <returns>Returns true whether mouse is physically over the popup</returns>
|
|
public static bool IsMousePhysicallyOver(Popup popup)
|
|
{
|
|
if (popup?.Child is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return IsMousePhysicallyOver(popup.Child);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true whether mouse is physically over the element
|
|
/// </summary>
|
|
/// <param name="element">Element</param>
|
|
/// <returns>Returns true whether mouse is physically over the element</returns>
|
|
public static bool IsMousePhysicallyOver(UIElement element)
|
|
{
|
|
if (element is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var position = Mouse.GetPosition(element);
|
|
return position.X >= 0.0
|
|
&& position.Y >= 0.0
|
|
&& position.X <= element.RenderSize.Width
|
|
&& position.Y <= element.RenderSize.Height;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles context menu opening event
|
|
/// </summary>
|
|
public static void OnContextMenuOpening(object sender, ContextMenuEventArgs e)
|
|
{
|
|
if (sender is IDropDownControl control)
|
|
{
|
|
control.IsContextMenuOpened = true;
|
|
WriteDebug("Context menu opening");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles context menu closing event
|
|
/// </summary>
|
|
public static void OnContextMenuClosing(object sender, ContextMenuEventArgs e)
|
|
{
|
|
if (sender is IDropDownControl control)
|
|
{
|
|
WriteDebug("Context menu closing");
|
|
control.IsContextMenuOpened = false;
|
|
|
|
if (Mouse.Captured is System.Windows.Controls.ContextMenu == false)
|
|
{
|
|
RaiseDismissPopupEvent(e.OriginalSource, DismissPopupMode.MouseNotOver);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool IsPopupRoot(object obj)
|
|
{
|
|
if (obj is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var type = obj.GetType();
|
|
|
|
return type.FullName == "System.Windows.Controls.Primitives.PopupRoot"
|
|
|| type.Name == "PopupRoot";
|
|
}
|
|
|
|
private static void WriteDebug(string message)
|
|
{
|
|
//Debug.WriteLine(message);
|
|
}
|
|
}
|
|
} |