mirror of
https://gitee.com/akwkevin/aistudio.-wpf.-diagram
synced 2026-04-07 17:56:35 +08:00
564 lines
18 KiB
C#
564 lines
18 KiB
C#
// ReSharper disable once CheckNamespace
|
||
namespace Fluent
|
||
{
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.ComponentModel;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
using System.Windows.Input;
|
||
using System.Windows.Interop;
|
||
using System.Windows.Threading;
|
||
using ControlzEx.Standard;
|
||
using Fluent.Internal;
|
||
|
||
/// <summary>
|
||
/// Handles Alt, F10 and so on
|
||
/// </summary>
|
||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||
public class KeyTipService
|
||
{
|
||
#region Fields
|
||
|
||
private ScopeGuard windowPreviewKeyDownScopeGuard;
|
||
|
||
// Host element, usually this is Ribbon
|
||
private readonly Ribbon ribbon;
|
||
|
||
// Timer to show KeyTips with delay
|
||
private readonly DispatcherTimer timer;
|
||
|
||
// Is KeyTips Actived now
|
||
private KeyTipAdorner activeAdornerChain;
|
||
// This element must be remembered to restore focus
|
||
private FocusWrapper backUpFocusedControl;
|
||
|
||
// Window where we attached
|
||
private Window window;
|
||
|
||
// Whether we attached to window
|
||
private bool attached;
|
||
|
||
// Attached HWND source
|
||
private HwndSource attachedHwndSource;
|
||
|
||
private string currentUserInput;
|
||
|
||
/// <summary>
|
||
/// Checks if any keytips are visible.
|
||
/// </summary>
|
||
public bool AreAnyKeyTipsVisible
|
||
{
|
||
get
|
||
{
|
||
if (this.activeAdornerChain != null)
|
||
{
|
||
return this.activeAdornerChain.AreAnyKeyTipsVisible;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static readonly Key[] modifierKeys =
|
||
{
|
||
Key.LeftShift,
|
||
Key.RightShift,
|
||
Key.LeftCtrl,
|
||
Key.RightCtrl,
|
||
Key.LeftAlt,
|
||
Key.RightAlt,
|
||
};
|
||
|
||
/// <summary>
|
||
/// The default keys used to activate key tips.
|
||
/// </summary>
|
||
public static IList<Key> DefaultKeyTipKeys =>
|
||
new List<Key>
|
||
{
|
||
Key.LeftAlt,
|
||
Key.RightAlt,
|
||
Key.F10
|
||
};
|
||
|
||
/// <summary>
|
||
/// List of key tip activation keys.
|
||
/// </summary>
|
||
public IList<Key> KeyTipKeys { get; } = DefaultKeyTipKeys;
|
||
|
||
#endregion
|
||
|
||
#region Initialization
|
||
|
||
/// <summary>
|
||
/// Default constrctor
|
||
/// </summary>
|
||
/// <param name="ribbon">Host element</param>
|
||
public KeyTipService(Ribbon ribbon)
|
||
{
|
||
this.ribbon = ribbon ?? throw new ArgumentNullException(nameof(ribbon));
|
||
|
||
// Initialize timer
|
||
this.timer = new DispatcherTimer(TimeSpan.FromSeconds(0.7), DispatcherPriority.SystemIdle, this.OnDelayedShow, Dispatcher.CurrentDispatcher);
|
||
this.timer.Stop();
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// Attaches self
|
||
/// </summary>
|
||
public void Attach()
|
||
{
|
||
if (this.attached
|
||
|| this.ribbon.IsKeyTipHandlingEnabled == false)
|
||
{
|
||
return;
|
||
}
|
||
|
||
this.attached = true;
|
||
|
||
// KeyTip service must not work in design mode
|
||
if (DesignerProperties.GetIsInDesignMode(this.ribbon))
|
||
{
|
||
return;
|
||
}
|
||
|
||
this.window = Window.GetWindow(this.ribbon);
|
||
if (this.window is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
this.window.PreviewKeyDown += this.OnWindowPreviewKeyDown;
|
||
this.window.KeyUp += this.OnWindowKeyUp;
|
||
|
||
// Hookup non client area messages
|
||
this.attachedHwndSource = (HwndSource)PresentationSource.FromVisual(this.window);
|
||
this.attachedHwndSource?.AddHook(this.WindowProc);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Detachs self
|
||
/// </summary>
|
||
public void Detach()
|
||
{
|
||
if (this.attached == false)
|
||
{
|
||
return;
|
||
}
|
||
|
||
this.attached = false;
|
||
|
||
// prevent delay show
|
||
this.timer.Stop();
|
||
|
||
if (this.window != null)
|
||
{
|
||
this.window.PreviewKeyDown -= this.OnWindowPreviewKeyDown;
|
||
this.window.KeyUp -= this.OnWindowKeyUp;
|
||
|
||
this.window = null;
|
||
}
|
||
|
||
// Unhook non client area messages
|
||
this.attachedHwndSource?.RemoveHook(this.WindowProc);
|
||
}
|
||
|
||
// Window's messages hook up
|
||
private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||
{
|
||
#pragma warning disable 618
|
||
var message = (WM)msg;
|
||
|
||
// We must terminate the keytip's adorner chain if:
|
||
if (message == WM.NCACTIVATE // mouse clicks in non client area
|
||
|| (message == WM.ACTIVATE && wParam == IntPtr.Zero) // the window is deactivated
|
||
|| (message >= WM.NCLBUTTONDOWN && message <= WM.NCXBUTTONDBLCLK) // mouse click (non client area)
|
||
|| (message >= WM.LBUTTONDOWN && message <= WM.MBUTTONDBLCLK)) // mouse click
|
||
{
|
||
if (this.activeAdornerChain?.IsAdornerChainAlive == true)
|
||
{
|
||
this.Terminate();
|
||
}
|
||
}
|
||
|
||
// Fix for #632.
|
||
// Yes this looks awkward, calling the PopupService here, but the alternative would be to let the PopupService know about windows.
|
||
if (message == WM.ACTIVATE
|
||
&& wParam == IntPtr.Zero) // the window is deactivated
|
||
{
|
||
PopupService.RaiseDismissPopupEvent(this.ribbon, DismissPopupMode.Always, DismissPopupReason.ApplicationLostFocus);
|
||
PopupService.RaiseDismissPopupEvent(Mouse.Captured, DismissPopupMode.Always, DismissPopupReason.ApplicationLostFocus);
|
||
PopupService.RaiseDismissPopupEvent(Keyboard.FocusedElement, DismissPopupMode.Always, DismissPopupReason.ApplicationLostFocus);
|
||
}
|
||
#pragma warning restore 618
|
||
|
||
return IntPtr.Zero;
|
||
}
|
||
|
||
private void OnWindowPreviewKeyDown(object sender, KeyEventArgs e)
|
||
{
|
||
if (this.windowPreviewKeyDownScopeGuard?.IsActive == true)
|
||
{
|
||
System.Media.SystemSounds.Beep.Play();
|
||
return;
|
||
}
|
||
|
||
using var scopeGuard = new ScopeGuard().Start();
|
||
this.windowPreviewKeyDownScopeGuard = scopeGuard;
|
||
|
||
if (e.IsRepeat
|
||
|| e.Handled)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (this.ribbon.IsCollapsed
|
||
|| this.ribbon.IsEnabled == false
|
||
|| this.window.IsActive == false)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Keytips should be cancelled if Alt+Num0 is pressed #241.
|
||
// This allows entering special keys via numpad.
|
||
if (e.KeyboardDevice.Modifiers == ModifierKeys.Alt
|
||
&& e.SystemKey >= Key.NumPad0
|
||
&& e.SystemKey <= Key.NumPad9)
|
||
{
|
||
this.Terminate();
|
||
return;
|
||
}
|
||
|
||
if (this.IsShowOrHideKey(e))
|
||
{
|
||
if (this.activeAdornerChain is null
|
||
|| this.activeAdornerChain.IsAdornerChainAlive == false
|
||
|| this.activeAdornerChain.AreAnyKeyTipsVisible == false)
|
||
{
|
||
this.ShowDelayed();
|
||
}
|
||
else
|
||
{
|
||
this.Terminate();
|
||
}
|
||
}
|
||
else if (e.Key == Key.Escape
|
||
&& this.activeAdornerChain != null)
|
||
{
|
||
this.activeAdornerChain.ActiveKeyTipAdorner.Back();
|
||
this.ClearUserInput();
|
||
e.Handled = true;
|
||
}
|
||
else if ((e.Key != Key.System && this.activeAdornerChain is null)
|
||
|| e.SystemKey == Key.Escape
|
||
|| (e.KeyboardDevice.Modifiers != ModifierKeys.Alt && this.activeAdornerChain is null))
|
||
{
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
var actualKey = e.Key == Key.System ? e.SystemKey : e.Key;
|
||
// we need to get the real string input for the key because of keys like <20>,<2C>,<2C> #258
|
||
var key = KeyEventUtility.GetStringFromKey(actualKey);
|
||
var isKeyRealInput = string.IsNullOrEmpty(key) == false
|
||
&& key != "\t";
|
||
|
||
// Don't do anything and let WPF handle the rest
|
||
if (isKeyRealInput == false)
|
||
{
|
||
// This block is a "temporary" fix for keyboard navigation not matching the office behavior.
|
||
// If someone finds a way to implement it properly, here is your starting point.
|
||
// In office: If you navigate by keyboard (in menus) and keytips are shown they are shown or hidden based on the menu you are in.
|
||
// Implementing navigation the way office does would require complex focus/state tracking etc. so i decided to just terminate keytips and not restore focus.
|
||
{
|
||
this.backUpFocusedControl = null;
|
||
this.Terminate();
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
var shownImmediately = false;
|
||
|
||
// Should we show the keytips and immediately react to key?
|
||
if (this.activeAdornerChain is null
|
||
|| this.activeAdornerChain.IsAdornerChainAlive == false
|
||
|| this.activeAdornerChain.AreAnyKeyTipsVisible == false)
|
||
{
|
||
this.ShowImmediatly();
|
||
shownImmediately = true;
|
||
}
|
||
|
||
if (this.activeAdornerChain is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var previousInput = this.currentUserInput;
|
||
this.currentUserInput += key;
|
||
|
||
if (this.activeAdornerChain.ActiveKeyTipAdorner.ContainsKeyTipStartingWith(this.currentUserInput) == false)
|
||
{
|
||
// Handles access-keys #258
|
||
if (shownImmediately)
|
||
{
|
||
this.Terminate();
|
||
return;
|
||
}
|
||
|
||
// If no key tips match the current input, continue with the previously entered and still correct keys.
|
||
this.currentUserInput = previousInput;
|
||
System.Media.SystemSounds.Beep.Play();
|
||
e.Handled = true;
|
||
return;
|
||
}
|
||
|
||
if (this.activeAdornerChain.ActiveKeyTipAdorner.Forward(this.currentUserInput, true))
|
||
{
|
||
this.ClearUserInput();
|
||
e.Handled = true;
|
||
return;
|
||
}
|
||
|
||
this.activeAdornerChain.ActiveKeyTipAdorner.FilterKeyTips(this.currentUserInput);
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
|
||
private void OnWindowKeyUp(object sender, KeyEventArgs e)
|
||
{
|
||
if (this.ribbon.IsCollapsed
|
||
|| this.ribbon.IsEnabled == false
|
||
|| this.window.IsActive == false)
|
||
{
|
||
this.Terminate();
|
||
return;
|
||
}
|
||
|
||
if (this.IsShowOrHideKey(e))
|
||
{
|
||
this.ClearUserInput();
|
||
|
||
if (this.timer.IsEnabled)
|
||
{
|
||
this.ShowImmediatly();
|
||
}
|
||
|
||
if (this.activeAdornerChain != null)
|
||
{
|
||
e.Handled = true;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
this.timer.Stop();
|
||
}
|
||
}
|
||
|
||
private bool IsShowOrHideKey(KeyEventArgs e)
|
||
{
|
||
var realKey = e.Key == Key.System
|
||
? e.SystemKey
|
||
: e.Key;
|
||
|
||
// Shift + F10 is meant to open the context menu. So we just ignore it.
|
||
if (realKey == Key.F10
|
||
&& (Keyboard.IsKeyDown(Key.LeftShift)
|
||
|| Keyboard.IsKeyDown(Key.RightShift)))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var isShowOrHideKey = this.KeyTipKeys.Any(x => x == realKey);
|
||
|
||
if (isShowOrHideKey == false)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var blacklistedModifierKeys = modifierKeys.Except(this.KeyTipKeys);
|
||
var blacklistedKeyPressed = blacklistedModifierKeys.Any(Keyboard.IsKeyDown);
|
||
return blacklistedKeyPressed == false;
|
||
}
|
||
|
||
private void ClearUserInput()
|
||
{
|
||
this.currentUserInput = string.Empty;
|
||
}
|
||
|
||
private void ClosePopups()
|
||
{
|
||
PopupService.RaiseDismissPopupEvent(Keyboard.FocusedElement, DismissPopupMode.Always, DismissPopupReason.ShowingKeyTips);
|
||
}
|
||
|
||
private void RestoreFocus()
|
||
{
|
||
this.backUpFocusedControl?.Focus();
|
||
this.backUpFocusedControl = null;
|
||
}
|
||
|
||
private void OnAdornerChainTerminated(object sender, KeyTipPressedResult e)
|
||
{
|
||
if (this.activeAdornerChain != null)
|
||
{
|
||
this.activeAdornerChain.Terminated -= this.OnAdornerChainTerminated;
|
||
}
|
||
|
||
this.activeAdornerChain = null;
|
||
this.ClearUserInput();
|
||
|
||
if (e.PressedElementOpenedPopup == false)
|
||
{
|
||
this.ClosePopups();
|
||
}
|
||
|
||
if (e.PressedElementAquiredFocus == false)
|
||
{
|
||
this.RestoreFocus();
|
||
}
|
||
}
|
||
|
||
private void OnDelayedShow(object sender, EventArgs e)
|
||
{
|
||
if (this.activeAdornerChain is null)
|
||
{
|
||
this.Show();
|
||
}
|
||
|
||
this.timer.Stop();
|
||
}
|
||
|
||
private void ShowImmediatly()
|
||
{
|
||
this.Show();
|
||
}
|
||
|
||
private void ShowDelayed()
|
||
{
|
||
this.Terminate();
|
||
|
||
this.timer.Start();
|
||
}
|
||
|
||
private void Terminate()
|
||
{
|
||
this.activeAdornerChain?.Terminate(KeyTipPressedResult.Empty);
|
||
}
|
||
|
||
private void Show()
|
||
{
|
||
this.timer.Stop();
|
||
|
||
// Check whether the window is
|
||
// - still present (prevents exceptions when window is closed by system commands)
|
||
// - still active (prevents keytips showing during Alt-Tab'ing)
|
||
if (this.window is null
|
||
|| this.window.IsActive == false)
|
||
{
|
||
this.RestoreFocus();
|
||
return;
|
||
}
|
||
|
||
// Special behavior for backstage, application menu and start screen.
|
||
// If one of those is open we have to forward key tips directly to them.
|
||
var keyTipsTarget = this.GetStartScreen()
|
||
?? this.GetBackstage()
|
||
?? this.GetApplicationMenu()
|
||
?? this.ribbon;
|
||
|
||
if (keyTipsTarget is null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
this.ClosePopups();
|
||
|
||
this.backUpFocusedControl = null;
|
||
|
||
// If focus is inside the Ribbon already we don't want to jump around after finishing with KeyTips
|
||
if (UIHelper.GetParent<Ribbon>(Keyboard.FocusedElement as DependencyObject) is null)
|
||
{
|
||
this.backUpFocusedControl = FocusWrapper.GetWrapperForCurrentFocus();
|
||
}
|
||
|
||
if (keyTipsTarget is Ribbon targetRibbon
|
||
&& targetRibbon.IsMinimized == false
|
||
&& targetRibbon.SelectedTabIndex >= 0
|
||
&& targetRibbon.TabControl != null)
|
||
{
|
||
// Focus ribbon
|
||
(this.ribbon.TabControl.ItemContainerGenerator.ContainerFromIndex(this.ribbon.TabControl.SelectedIndex) as UIElement)?.Focus();
|
||
}
|
||
|
||
this.ClearUserInput();
|
||
|
||
if (this.activeAdornerChain != null)
|
||
{
|
||
this.activeAdornerChain.Terminated -= this.OnAdornerChainTerminated;
|
||
}
|
||
|
||
this.activeAdornerChain = new KeyTipAdorner(keyTipsTarget, keyTipsTarget, null);
|
||
this.activeAdornerChain.Terminated += this.OnAdornerChainTerminated;
|
||
this.activeAdornerChain.Attach();
|
||
}
|
||
|
||
private FrameworkElement GetBackstage()
|
||
{
|
||
if (this.ribbon.Menu is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var control = this.ribbon.Menu as Backstage ?? UIHelper.FindImmediateVisualChild<Backstage>(this.ribbon.Menu, IsVisible);
|
||
|
||
if (control is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return control.IsOpen
|
||
? control
|
||
: null;
|
||
}
|
||
|
||
private FrameworkElement GetApplicationMenu()
|
||
{
|
||
if (this.ribbon.Menu is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var control = this.ribbon.Menu as ApplicationMenu ?? UIHelper.FindImmediateVisualChild<ApplicationMenu>(this.ribbon.Menu, IsVisible);
|
||
|
||
if (control is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return control.IsDropDownOpen
|
||
? control
|
||
: null;
|
||
}
|
||
|
||
private FrameworkElement GetStartScreen()
|
||
{
|
||
var control = this.ribbon.StartScreen;
|
||
|
||
if (control is null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return control.IsOpen
|
||
? control
|
||
: null;
|
||
}
|
||
|
||
private static bool IsVisible(FrameworkElement obj)
|
||
{
|
||
return obj.Visibility == Visibility.Visible;
|
||
}
|
||
}
|
||
} |