From 5643e23430c44cae8321f154491024b1d163fb37 Mon Sep 17 00:00:00 2001 From: Martin Zikmund Date: Wed, 12 Jun 2024 12:52:13 +0200 Subject: [PATCH] feat: Add support for Keyboard Accelerator tooltips --- src/Uno.UI/DirectUI/DXamlTestHooks.cs | 6 +- .../ToolTip/ToolTipService.Properties.cs | 2 +- .../Xaml/Controls/ToolTip/ToolTipService.cs | 106 ++++++++---------- .../Controls/ToolTip/ToolTipService.mux.cs | 68 +++++++++++ 4 files changed, 117 insertions(+), 65 deletions(-) create mode 100644 src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.mux.cs diff --git a/src/Uno.UI/DirectUI/DXamlTestHooks.cs b/src/Uno.UI/DirectUI/DXamlTestHooks.cs index 02b0a69afa10..725deb5b7a75 100644 --- a/src/Uno.UI/DirectUI/DXamlTestHooks.cs +++ b/src/Uno.UI/DirectUI/DXamlTestHooks.cs @@ -4,9 +4,5 @@ namespace Uno.UI.DirectUI; internal static class DXamlTestHooks { - public static ToolTip TestGetActualToolTip(UIElement element) - { - // TODO:MZ: Should be GetActualToolTipObject - return ToolTipService.GetToolTipReference(element); - } + public static ToolTip TestGetActualToolTip(UIElement element) => ToolTipService.GetActualToolTipObject(element); } diff --git a/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.Properties.cs b/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.Properties.cs index d606ad59870f..ff9a38e45e62 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.Properties.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.Properties.cs @@ -77,7 +77,7 @@ public partial class ToolTipService "KeyboardAcceleratorToolTipObject", typeof(ToolTip), typeof(ToolTipService), - new FrameworkPropertyMetadata(default, OnToolTipChanged)); + new FrameworkPropertyMetadata(default)); internal static ToolTip GetKeyboardAcceleratorToolTipObject(DependencyObject element) => (ToolTip)element.GetValue(KeyboardAcceleratorToolTipObjectProperty); diff --git a/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.cs b/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.cs index d92ca218e5e4..9534443c890b 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.cs @@ -1,12 +1,10 @@ using System; -using System.Threading.Tasks; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; -using Uno.UI; using Uno.Disposables; +using Uno.UI; using Windows.System; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; + #if __IOS__ using UIKit; @@ -16,6 +14,9 @@ namespace Microsoft.UI.Xaml.Controls; +/// +/// Represents a service that provides static methods to display a ToolTip. +/// public partial class ToolTipService { private static ToolTip m_CurrentToolTip; @@ -23,76 +24,63 @@ public partial class ToolTipService private static DispatcherTimer m_OpenTimer; private static DispatcherTimer m_CloseTimer; - private static void OnToolTipChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + private static void RegisterToolTip( + DependencyObject owner, + FrameworkElement container, + object toolTipAsObject, + bool isKeyboardAcceleratorToolTip) { if (!FeatureConfiguration.ToolTip.UseToolTips) { - return; // ToolTips are disabled + // ToolTips are disabled. + return; } - if (!(dependencyObject is FrameworkElement owner)) + if (owner is null || container is null) { + // ToolTip must have an owner. return; } - if (e.NewValue is null) + var toolTip = ConvertToToolTip(toolTipAsObject); + + toolTip.Placement = GetPlacement(toolTip); + toolTip.SetAnchor(GetPlacementTarget(container) ?? container); + + if (isKeyboardAcceleratorToolTip) { - DisposePreviousToolTip(); + ToolTipService.SetKeyboardAcceleratorToolTipObject(owner, toolTip); } - else if (e.NewValue is ToolTip newToolTip) + else { - var previousToolTip = GetToolTipReference(owner); + ToolTipService.SetToolTipReference(owner, toolTip); + } - // dispose the previous tooltip - if (previousToolTip != null && newToolTip != previousToolTip) - { - DisposePreviousToolTip(previousToolTip); - } + toolTip.OwnerEventSubscriptions = SubscribeToEvents(container, toolTip); + } - // setup new tooltip - if (newToolTip != previousToolTip) - { - SetupToolTip(newToolTip); - } + private static void UnregisterToolTip(DependencyObject owner, FrameworkElement container, bool isKeyboardAcceleratorToolTip) + { + ToolTip toolTipReference = null; + if (isKeyboardAcceleratorToolTip) + { + toolTipReference = ToolTipService.GetKeyboardAcceleratorToolTipObject(owner); } else { - var previousToolTip = GetToolTipReference(owner); - if (e.OldValue is ToolTip oldPrevious && oldPrevious == previousToolTip) - { - // dispose and setup a new tooltip - // to avoid corrupting previous tooltip's content with new value - DisposePreviousToolTip(previousToolTip); - SetupToolTip(new ToolTip { Content = e.NewValue }); - } - else if (previousToolTip != null) - { - // update the old tooltip with new content - previousToolTip.Content = e.NewValue; - } - else - { - // setup a new tooltip - SetupToolTip(new ToolTip { Content = e.NewValue }); - } + toolTipReference = ToolTipService.GetToolTipReference(owner); } - void SetupToolTip(ToolTip toolTip) + if (toolTipReference is null) { - toolTip.Placement = GetPlacement(toolTip); - toolTip.SetAnchor(GetPlacementTarget(owner) ?? owner); - - SetToolTipReference(owner, toolTip); - toolTip.OwnerEventSubscriptions = SubscribeToEvents(owner, toolTip); + return; } - void DisposePreviousToolTip(ToolTip toolTip = null) - { - toolTip ??= GetToolTipReference(owner); - toolTip.OwnerEventSubscriptions?.Dispose(); - toolTip.OwnerEventSubscriptions = null; - SetToolTipReference(owner, null); - } + toolTipReference.OwnerEventSubscriptions?.Dispose(); + toolTipReference.OwnerEventSubscriptions = null; + CloseToolTipImpl(toolTipReference); + + owner.ClearValue(isKeyboardAcceleratorToolTip ? KeyboardAcceleratorToolTipObjectProperty : ToolTipReferenceProperty); } private static void OnPlacementChanged(DependencyObject dependencyobject, DependencyPropertyChangedEventArgs e) @@ -196,7 +184,7 @@ private static void OnOwnerVisibilityChanged(DependencyObject sender, Dependency private static void OnOwnerLoaded(object sender, RoutedEventArgs e) { - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { owner.PointerEntered += OnPointerEntered; owner.PointerExited += OnPointerExited; @@ -216,7 +204,7 @@ private static void OnOwnerLoaded(object sender, RoutedEventArgs e) private static void OnOwnerUnloaded(object sender, RoutedEventArgs e) { - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { CloseToolTipImpl(toolTip); @@ -240,7 +228,7 @@ private static void OnPointerEntered(object sender, PointerRoutedEventArgs e) // so we are dropping any subsequent events from this frame-id. if (e.FrameId == m_LastEnteredFrameId) return; - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { if (toolTip.IsOpen) return; @@ -257,7 +245,7 @@ private static void OnPointerEntered(object sender, PointerRoutedEventArgs e) private static void OnPointerExited(object sender, PointerRoutedEventArgs e) { - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { CloseToolTipImpl(toolTip); } @@ -265,7 +253,7 @@ private static void OnPointerExited(object sender, PointerRoutedEventArgs e) private static void OnTapped(object sender, TappedRoutedEventArgs e) { - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { CloseToolTipImpl(toolTip); } @@ -273,7 +261,7 @@ private static void OnTapped(object sender, TappedRoutedEventArgs e) private static void OnKeyDown(object sender, KeyRoutedEventArgs args) { - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { switch (args.Key) { @@ -290,7 +278,7 @@ private static void OnKeyDown(object sender, KeyRoutedEventArgs args) private static void OnPointerPressed(object sender, PointerRoutedEventArgs e) { - if (sender is FrameworkElement owner && GetToolTipReference(owner) is { } toolTip) + if (sender is FrameworkElement owner && GetActualToolTipObject(owner) is { } toolTip) { if (e.GetCurrentPoint(owner).Properties.IsLeftButtonPressed) { diff --git a/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.mux.cs b/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.mux.cs new file mode 100644 index 000000000000..4a4725e448e9 --- /dev/null +++ b/src/Uno.UI/UI/Xaml/Controls/ToolTip/ToolTipService.mux.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. +// MUX Reference dxaml\xcp\dxaml\lib\ToolTipService_Partial.cpp, tag winui3/release/1.5.4, commit 98a60c8f30c84f297a175dd2884d54ecd1c8a4a9 +// Contains ported portions of dxaml\xcp\dxaml\lib\ToolTipService_Partial.cpp + +#nullable enable + +using static Uno.UI.FeatureConfiguration; + +namespace Microsoft.UI.Xaml.Controls; + +partial class ToolTipService +{ + private static void OnToolTipChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + if (sender is FrameworkElement senderAsFe) + { + bool isKeyboardAcceleratorToolTip = args.Property == ToolTipService.KeyboardAcceleratorToolTipProperty; + if (args.OldValue is not UnsetValue && args.OldValue is not null) + { + ToolTipService.UnregisterToolTip(sender, senderAsFe, isKeyboardAcceleratorToolTip); + } + + if (args.NewValue is { } toolTip) + { + ToolTipService.RegisterToolTip(sender, senderAsFe, toolTip, isKeyboardAcceleratorToolTip); + } + } + } + + private static ToolTip ConvertToToolTip(object objectIn) + { + if (objectIn is not ToolTip toolTip) + { + if (objectIn is FrameworkElement frameworkElement) + { + var objectInParent = objectIn.GetParent(); + if (objectInParent is ToolTip parentToolTip) + { + return parentToolTip; + } + } + + var newToolTip = new ToolTip + { + Content = objectIn + }; + + toolTip = newToolTip; + } + + return toolTip; + } + + internal static ToolTip? GetActualToolTipObject(DependencyObject element) + { + // Try to get the actual public tooltip object + var toolTip = ToolTipService.GetToolTipReference(element); + + // If public tooltip doesn't exist, then look for keyboard accelerator tooltip. + if (toolTip is null) + { + toolTip = ToolTipService.GetKeyboardAcceleratorToolTipObject(element); + } + + return toolTip; + } +}