Files
aistudio-wpf-diagram/Others/Fluent.Ribbon/Fluent.Ribbon/Services/KeyTipService.cs
2023-04-16 20:11:40 +08:00

564 lines
18 KiB
C#
Raw Blame History

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