|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using MS.Internal;
using MS.Internal.KnownBoxes;
using MS.Win32;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
namespace System.Windows.Controls
{
/// <summary>
/// Service class that provides the input for the ContextMenu and ToolTip services.
/// </summary>
internal sealed class PopupControlService
{
#region Creation
internal PopupControlService()
{
InputManager.Current.PostProcessInput += new ProcessInputEventHandler(OnPostProcessInput);
}
#endregion
#region Input Handling
/////////////////////////////////////////////////////////////////////
private void OnPostProcessInput(object sender, ProcessInputEventArgs e)
{
if (e.StagingItem.Input.RoutedEvent == InputManager.InputReportEvent)
{
InputReportEventArgs report = (InputReportEventArgs)e.StagingItem.Input;
if (!report.Handled)
{
if (report.Report.Type == InputType.Mouse)
{
RawMouseInputReport mouseReport = (RawMouseInputReport)report.Report;
if ((mouseReport.Actions & RawMouseActions.AbsoluteMove) == RawMouseActions.AbsoluteMove)
{
if ((Mouse.LeftButton == MouseButtonState.Pressed) ||
(Mouse.RightButton == MouseButtonState.Pressed))
{
DismissToolTips();
}
else
{
IInputElement directlyOver = Mouse.PrimaryDevice.RawDirectlyOver;
if (directlyOver != null)
{
// If possible, check that the mouse position is within the render bounds
// (avoids mouse capture confusion).
if (Mouse.CapturedMode != CaptureMode.None)
{
// Get the root visual
PresentationSource source = PresentationSource.CriticalFromVisual((DependencyObject)directlyOver);
UIElement rootAsUIElement = source != null ? source.RootVisual as UIElement : null;
if (rootAsUIElement != null)
{
// Get mouse position wrt to root
Point pt = Mouse.PrimaryDevice.GetPosition(rootAsUIElement);
// Hittest to find the element the mouse is over
IInputElement enabledHit;
rootAsUIElement.InputHitTest(pt, out enabledHit, out directlyOver);
}
else
{
directlyOver = null;
}
}
if (directlyOver != null)
{
// Process the mouse move
OnMouseMove(directlyOver);
}
}
}
}
else if ((mouseReport.Actions & RawMouseActions.Deactivate) == RawMouseActions.Deactivate)
{
DismissToolTips();
LastMouseDirectlyOver = null;
// When the user moves the cursor outside of the window,
// clear the LastMouseToolTipOwner property so if the user returns
// the mouse to the same item, the tooltip will reappear. If
// the deactivation is coming from a window grabbing capture
// (such as Drag and Drop) do not clear the property.
if (MS.Win32.SafeNativeMethods.GetCapture() == IntPtr.Zero)
{
LastMouseToolTipOwner = null;
}
}
}
}
}
else if (e.StagingItem.Input.RoutedEvent == Keyboard.KeyDownEvent)
{
ProcessKeyDown(sender, (KeyEventArgs)e.StagingItem.Input);
}
else if (e.StagingItem.Input.RoutedEvent == Keyboard.KeyUpEvent)
{
ProcessKeyUp(sender, (KeyEventArgs)e.StagingItem.Input);
}
else if (e.StagingItem.Input.RoutedEvent == Mouse.MouseUpEvent)
{
ProcessMouseUp(sender, (MouseButtonEventArgs)e.StagingItem.Input);
}
else if (e.StagingItem.Input.RoutedEvent == Mouse.MouseDownEvent)
{
DismissToolTips();
}
else if (e.StagingItem.Input.RoutedEvent == Keyboard.GotKeyboardFocusEvent)
{
ProcessGotKeyboardFocus(sender, (KeyboardFocusChangedEventArgs)e.StagingItem.Input);
}
else if (e.StagingItem.Input.RoutedEvent == Keyboard.LostKeyboardFocusEvent)
{
ProcessLostKeyboardFocus(sender, (KeyboardFocusChangedEventArgs)e.StagingItem.Input);
}
}
private void OnMouseMove(IInputElement directlyOver)
{
if (MouseHasLeftSafeArea())
{
DismissCurrentToolTip();
}
if (directlyOver != LastMouseDirectlyOver)
{
LastMouseDirectlyOver = directlyOver;
DependencyObject owner = FindToolTipOwner(directlyOver, ToolTipService.TriggerAction.Mouse);
BeginShowToolTip(owner, ToolTipService.TriggerAction.Mouse);
}
else
{
if (PendingToolTipTimer?.Tag == BooleanBoxes.TrueBox)
{
// the pending tooltip is on a short delay (see BeginShowToolTip)
if (CurrentToolTip == null)
{
// the mouse left the safe area - promote the pending tooltip now
PendingToolTipTimer.Stop();
PromotePendingToolTipToCurrent(ToolTipService.TriggerAction.Mouse);
}
else
{
// the mouse is still in the safe area - restart the timer
PendingToolTipTimer.Stop();
PendingToolTipTimer.Start();
}
}
}
}
/////////////////////////////////////////////////////////////////////
private void ProcessGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
// any focus change dismisses tooltips triggered from the keyboard
DismissKeyboardToolTips();
// focus changes caused by keyboard navigation can show a tooltip
if (KeyboardNavigation.IsKeyboardMostRecentInputDevice())
{
IInputElement focusedElement = e.NewFocus;
DependencyObject owner = FindToolTipOwner(focusedElement, ToolTipService.TriggerAction.KeyboardFocus);
BeginShowToolTip(owner, ToolTipService.TriggerAction.KeyboardFocus);
}
}
/////////////////////////////////////////////////////////////////////
private void ProcessLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
// any focus change dismisses tooltips triggered from the keyboard
DismissKeyboardToolTips();
}
/////////////////////////////////////////////////////////////////////
private void ProcessMouseUp(object sender, MouseButtonEventArgs e)
{
DismissToolTips();
if (!e.Handled)
{
if ((e.ChangedButton == MouseButton.Right) &&
(e.RightButton == MouseButtonState.Released))
{
IInputElement directlyOver = Mouse.PrimaryDevice.RawDirectlyOver;
if (directlyOver != null)
{
Point pt = Mouse.PrimaryDevice.GetPosition(directlyOver);
if (RaiseContextMenuOpeningEvent(directlyOver, pt.X, pt.Y,e.UserInitiated))
{
e.Handled = true;
}
}
}
}
}
/////////////////////////////////////////////////////////////////////
private void ProcessKeyDown(object sender, KeyEventArgs e)
{
if (!e.Handled)
{
const ModifierKeys ModifierMask = ModifierKeys.Alt | ModifierKeys.Control | ModifierKeys.Shift | ModifierKeys.Windows;
ModifierKeys modifierKeys = Keyboard.Modifiers & ModifierMask;
if ((e.SystemKey == Key.F10) && (modifierKeys == (ModifierKeys.Control | ModifierKeys.Shift)))
{
e.Handled = OpenOrCloseToolTipViaShortcut();
}
else if ((e.SystemKey == Key.F10) && (modifierKeys == ModifierKeys.Shift))
{
RaiseContextMenuOpeningEvent(e);
}
// track the last key-down, to detect Ctrl-KeyUp trigger
_lastCtrlKeyDown = Key.None;
if ((CurrentToolTip?.FromKeyboard ?? false) && (modifierKeys == ModifierKeys.Control) &&
(e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl))
{
_lastCtrlKeyDown = e.Key;
}
}
}
/////////////////////////////////////////////////////////////////////
private void ProcessKeyUp(object sender, KeyEventArgs e)
{
if (!e.Handled)
{
if (e.Key == Key.Apps)
{
RaiseContextMenuOpeningEvent(e);
}
// dismiss the keyboard ToolTip when user presses and releases Ctrl
if ((_lastCtrlKeyDown != Key.None) && (e.Key == _lastCtrlKeyDown) &&
(Keyboard.Modifiers == ModifierKeys.None) && (CurrentToolTip?.FromKeyboard ?? false))
{
DismissCurrentToolTip();
}
_lastCtrlKeyDown = Key.None;
}
}
#endregion
#region ToolTip
private bool OpenOrCloseToolTipViaShortcut()
{
DependencyObject owner = FindToolTipOwner(Keyboard.FocusedElement, ToolTipService.TriggerAction.KeyboardShortcut);
if (owner == null)
return false;
// if the owner's tooltip is open, dismiss it. Otherwise, show it.
if (owner == GetOwner(CurrentToolTip))
{
DismissCurrentToolTip();
}
else
{
if (owner == GetOwner(PendingToolTip))
{
// discard a previous pending request, so that the new one isn't ignored.
// This ensures that the tooltip opens immediately.
DismissPendingToolTip();
}
BeginShowToolTip(owner, ToolTipService.TriggerAction.KeyboardShortcut);
}
return true;
}
/// <summary>
/// Initiate the process of showing a tooltip.
/// Make a pending request, updating the pending and history state accordingly.
/// Prepare to promote the pending tooltip to "current", which happens either
/// immediately or after a delay.
/// </summary>
/// <param name="o">The tooltip owner</param>
/// <param name="triggerAction">The action that triggered showing the tooltip</param>
private void BeginShowToolTip(DependencyObject o, ToolTipService.TriggerAction triggerAction)
{
if (triggerAction == ToolTipService.TriggerAction.Mouse)
{
// ignore a mouse request if the mouse hasn't moved off the owner since the last mouse request
if (o == LastMouseToolTipOwner)
return;
LastMouseToolTipOwner = o;
// cancel a pending mouse request if the mouse has moved off its owner
if (PendingToolTip != null && !PendingToolTip.FromKeyboard && o != GetOwner(PendingToolTip))
{
DismissPendingToolTip();
}
}
// ignore a request if no owner, or already showing or pending its tooltip
if (o == null || o == GetOwner(PendingToolTip) || o == GetOwner(CurrentToolTip))
return;
// discard the previous pending request
DismissPendingToolTip();
// record a pending request
PendingToolTip = SentinelToolTip(o, triggerAction);
// decide when to promote to current
bool useShortDelay = false;
bool showNow = _quickShow;
if (!showNow)
{
ToolTip toReplace = CurrentToolTip;
switch (triggerAction)
{
case ToolTipService.TriggerAction.Mouse:
if (SafeArea != null)
{
// the mouse has moved over a tooltip owner o, while still
// within the safe area of the current tooltip (which must be from mouse).
// This is an ambiguous case - the user could be trying to move the
// mouse toward the tooltip or they could be trying to move the
// mouse over o. There's no way to know the user's intent.
// But the expected response is much different: in the first
// case we should leave the current tooltip open, in the second
// we should replace it with o's tooltip.
//
// We use a heuristic to compromise between these conflicting expectations.
// We'll put the pending request on a timer with a very short interval.
// If the user moves the mouse within the interval, we restart the timer;
// this keeps the tooltip open as long as the user keeps moving the mouse.
// But if the timer expires, we promote the pending request;
// this shows o's tooltip shortly after the user stops moving the mouse (or
// moves it outside the current safe area).
useShortDelay = true;
}
break;
case ToolTipService.TriggerAction.KeyboardFocus:
// a focus request shows without delay if the current tooltip also came from keyboard
showNow = toReplace?.FromKeyboard ?? false;
break;
case ToolTipService.TriggerAction.KeyboardShortcut:
default:
// an explicit keystroke request always shows without delay
toReplace = null;
showNow = true;
break;
}
// replacing a tooltip with BetweenShowDelay=0 should invoke the delay
if (toReplace != null && (showNow || useShortDelay))
{
DependencyObject currentOwner = GetOwner(toReplace);
if (ToolTipService.GetBetweenShowDelay(currentOwner) == 0)
{
showNow = false;
useShortDelay = false;
}
}
}
// promote now, or schedule delayed promotion
int showDelay = (showNow ? 0 : useShortDelay ? ShortDelay : ToolTipService.GetInitialShowDelay(o));
if (showDelay == 0)
{
PromotePendingToolTipToCurrent(triggerAction);
}
else
{
PendingToolTipTimer = new DispatcherTimer(DispatcherPriority.Normal);
PendingToolTipTimer.Interval = TimeSpan.FromMilliseconds(showDelay);
PendingToolTipTimer.Tick += new EventHandler((s, e) => { PromotePendingToolTipToCurrent(triggerAction); });
PendingToolTipTimer.Tag = BooleanBoxes.Box(useShortDelay);
PendingToolTipTimer.Start();
}
}
private void PromotePendingToolTipToCurrent(ToolTipService.TriggerAction triggerAction)
{
DependencyObject o = GetOwner(PendingToolTip);
DismissToolTips();
if (o != null)
{
ShowToolTip(o, ToolTipService.IsFromKeyboard(triggerAction));
}
}
/// <summary>
/// Initiates the process of opening the tooltip popup,
/// and makes the tooltip "current".
/// </summary>
/// <param name="o">The owner of the tooltip</param>
/// <param name="fromKeyboard">True if the tooltip is triggered by keyboard</param>
private void ShowToolTip(DependencyObject o, bool fromKeyboard)
{
Debug.Assert(_currentToolTip == null);
ResetCurrentToolTipTimer();
OnForceClose(null, EventArgs.Empty);
bool show = true;
IInputElement element = o as IInputElement;
if (element != null)
{
ToolTipEventArgs args = new ToolTipEventArgs(opening:true);
// ** Public callout - re-entrancy is possible **//
element.RaiseEvent(args);
// [re-examine _currentToolTip, re-entrancy can change it]
show = !args.Handled && (_currentToolTip == null);
}
if (show)
{
object tooltip = ToolTipService.GetToolTip(o);
ToolTip tip = tooltip as ToolTip;
if (tip != null)
{
_currentToolTip = tip;
}
else
{
_currentToolTip = new ToolTip();
_currentToolTip.SetValue(ServiceOwnedProperty, BooleanBoxes.TrueBox);
// Bind the content of the tooltip to the ToolTip attached property
Binding binding = new Binding();
binding.Path = new PropertyPath(ToolTipService.ToolTipProperty);
binding.Mode = BindingMode.OneWay;
binding.Source = o;
_currentToolTip.SetBinding(ToolTip.ContentProperty, binding);
}
if (!_currentToolTip.StaysOpen)
{
// The popup takes capture in this case, which causes us to hit test to the wrong window.
// We do not support this scenario. Cleanup and then throw and exception.
throw new NotSupportedException(SR.ToolTipStaysOpenFalseNotAllowed);
}
_currentToolTip.SetValue(OwnerProperty, o);
_currentToolTip.Closed += OnToolTipClosed;
_currentToolTip.FromKeyboard = fromKeyboard;
if (!_currentToolTip.IsOpen)
{
// open the tooltip, and finish the initialization when its popup window is available.
_currentToolTip.Opened += OnToolTipOpened;
_currentToolTip.IsOpen = true;
}
else
{
// If the tooltip is already open, initialize it now. This only happens when the
// app manages the tooltip directly.
SetSafeArea(_currentToolTip);
}
CurrentToolTipTimer = new DispatcherTimer(DispatcherPriority.Normal);
CurrentToolTipTimer.Interval = TimeSpan.FromMilliseconds(ToolTipService.GetShowDuration(o));
CurrentToolTipTimer.Tick += new EventHandler(OnShowDurationTimerExpired);
CurrentToolTipTimer.Start();
}
}
private void OnShowDurationTimerExpired(object sender, EventArgs e)
{
DismissCurrentToolTip();
}
// called from ToolTip.OnContentChanged, when the owner of the current
// tooltip changes its ToolTip property from a non-ToolTip to a ToolTip.
internal void ReplaceCurrentToolTip()
{
ToolTip currentToolTip = _currentToolTip;
if (currentToolTip == null)
return;
// get information from the current tooltip, before it goes away
DependencyObject owner = GetOwner(currentToolTip);
bool fromKeyboard = currentToolTip.FromKeyboard;
// dismiss the current tooltip, then show a new one in its stead
DismissCurrentToolTip();
ShowToolTip(owner, fromKeyboard);
}
internal void DismissToolTipsForOwner(DependencyObject o)
{
if (o == GetOwner(PendingToolTip))
{
DismissPendingToolTip();
}
if (o == GetOwner(CurrentToolTip))
{
DismissCurrentToolTip();
}
}
private void DismissToolTips()
{
DismissPendingToolTip();
DismissCurrentToolTip();
}
private void DismissKeyboardToolTips()
{
if (PendingToolTip?.FromKeyboard ?? false)
{
DismissPendingToolTip();
}
if (CurrentToolTip?.FromKeyboard ?? false)
{
DismissCurrentToolTip();
}
}
private void DismissPendingToolTip()
{
if (PendingToolTipTimer != null)
{
PendingToolTipTimer.Stop();
PendingToolTipTimer = null;
}
if (PendingToolTip != null)
{
PendingToolTip = null;
_sentinelToolTip.SetValue(OwnerProperty, null);
}
}
private void DismissCurrentToolTip()
{
ToolTip currentToolTip = _currentToolTip;
_currentToolTip = null;
CloseToolTip(currentToolTip);
}
// initiate the process of closing the tooltip's popup.
private void CloseToolTip(ToolTip tooltip)
{
if (tooltip == null)
return;
SetSafeArea(null);
ResetCurrentToolTipTimer();
// cache the owner now, in case re-entrancy clears it
DependencyObject owner = GetOwner(tooltip);
try
{
// notify listeners that the tooltip is closing
if (tooltip.IsOpen)
{
IInputElement element = owner as IInputElement;
if (element != null)
{
// ** Public callout - re-entrancy is possible **//
element.RaiseEvent(new ToolTipEventArgs(opening:false));
}
}
}
finally
{
// close the tooltip popup
// [re-examine IsOpen - re-entrancy could change it]
if (tooltip.IsOpen)
{
// ** Public callout - re-entrancy is possible **//
tooltip.IsOpen = false;
// allow time for the popup's fade-out or slide animation
_forceCloseTimer = new DispatcherTimer(DispatcherPriority.Normal);
_forceCloseTimer.Interval = Popup.AnimationDelayTime;
_forceCloseTimer.Tick += new EventHandler(OnForceClose);
_forceCloseTimer.Tag = tooltip;
_forceCloseTimer.Start();
// begin the BetweenShowDelay interval, during which another tooltip
// can open without the usual delay
int betweenShowDelay = ToolTipService.GetBetweenShowDelay(owner);
_quickShow = (betweenShowDelay > 0);
if (_quickShow)
{
CurrentToolTipTimer = new DispatcherTimer(DispatcherPriority.Normal);
CurrentToolTipTimer.Interval = TimeSpan.FromMilliseconds(betweenShowDelay);
CurrentToolTipTimer.Tick += new EventHandler(OnBetweenShowDelay);
CurrentToolTipTimer.Start();
}
}
else
{
ClearServiceProperties(tooltip);
}
}
}
/// <summary>
/// Clean up any service-only properties we may have set on the given tooltip
/// </summary>
/// <param name="tooltip"></param>
private void ClearServiceProperties(ToolTip tooltip)
{
// This is normally called from OnToolTipClosed, after CloseToolTip has closed the tooltip
// and waited for the tooltip's Popup to destroy its window asynchronously.
// Apps can close the Popup directly (not easily done, and not recommended), which leads to
// a call from OnToolTipClosed while tooltip.IsOpen is still true. In that case we need to
// leave the properties in place - CloseToolTip needs them (as does the popup if it should
// re-open). They will get cleared by OnForceClose, if not earlier.
if (tooltip != null && !tooltip.IsOpen)
{
tooltip.ClearValue(OwnerProperty);
tooltip.FromKeyboard = false;
tooltip.Closed -= OnToolTipClosed;
if ((bool)tooltip.GetValue(ServiceOwnedProperty))
{
BindingOperations.ClearBinding(tooltip, ToolTip.ContentProperty);
}
}
}
private DependencyObject FindToolTipOwner(IInputElement element, ToolTipService.TriggerAction triggerAction)
{
if (element == null)
return null;
DependencyObject owner = null;
switch (triggerAction)
{
case ToolTipService.TriggerAction.Mouse:
// look up the tree for the nearest tooltip owner
FindToolTipEventArgs args = new FindToolTipEventArgs(triggerAction);
element.RaiseEvent(args);
owner = args.TargetElement;
break;
case ToolTipService.TriggerAction.KeyboardFocus:
case ToolTipService.TriggerAction.KeyboardShortcut:
// use the element itself, if it is a tooltip owner
owner = element as DependencyObject;
if (owner != null && !ToolTipService.ToolTipIsEnabled(owner, triggerAction))
{
owner = null;
}
break;
}
// ignore nested tooltips
if (WithinCurrentToolTip(owner))
{
owner = null;
}
return owner;
}
private bool WithinCurrentToolTip(DependencyObject o)
{
// If no current tooltip, then no need to look
if (_currentToolTip == null)
{
return false;
}
DependencyObject v = o as Visual;
if (v == null)
{
ContentElement ce = o as ContentElement;
if (ce != null)
{
v = FindContentElementParent(ce);
}
else
{
v = o as Visual3D;
}
}
return (v != null) &&
((v is Visual && ((Visual)v).IsDescendantOf(_currentToolTip)) ||
(v is Visual3D && ((Visual3D)v).IsDescendantOf(_currentToolTip)));
}
private void ResetCurrentToolTipTimer()
{
if (CurrentToolTipTimer != null)
{
CurrentToolTipTimer.Stop();
CurrentToolTipTimer = null;
_quickShow = false;
}
}
/// <summary>
/// Event handler for ToolTip.Opened
/// </summary>
private void OnToolTipOpened(object sender, EventArgs e)
{
ToolTip toolTip = (ToolTip)sender;
toolTip.Opened -= OnToolTipOpened;
SetSafeArea(toolTip);
}
// Clear service properties when tooltip has closed
private void OnToolTipClosed(object sender, EventArgs e)
{
ToolTip toolTip = (ToolTip)sender;
if (toolTip != CurrentToolTip)
{
// If we manage the tooltip (the normal case), the current tooltip closes via
// 1. DismissCurrentToolTip sets _currentToolTip=null and calls CloseToolTip
// 2. CloseToolTip sets toolTip.IsOpen=false, and returns
// 3. Asynchronously, the tooltip raises the Closed event (after popup animations have run)
// 4. our event handler OnToolTipClosed gets here
// It's now time to do the final cleanup, which includes removing this event handler.
ClearServiceProperties(toolTip);
}
else
{
// We get here if the app closes the current tooltip or its popup directly.
// Do nothing (i.e. ignore the event). This leaves the service properties in place -
// eventually DismissCurrentToolTip will call CloseToolTip, which needs them
// (in particular the Owner property). When that happens, either
// a. tooltip.IsOpen==false. CloseToolTip clears the service properties immediately.
// b. tooltip.IsOpen==true. (This can happen if the app re-opens the tooltip directly.)
// CloseToolTip proceeds as in step 2 of the normal case.
// Either way, the final cleanup happens.
}
}
// The previous tooltip hasn't closed and we are trying to open a new one
private void OnForceClose(object sender, EventArgs e)
{
if (_forceCloseTimer != null)
{
_forceCloseTimer.Stop();
ToolTip toolTip = (ToolTip)_forceCloseTimer.Tag;
toolTip.ForceClose();
ClearServiceProperties(toolTip); // this handles the case where app closed the Popup directly
_forceCloseTimer = null;
}
}
private void OnBetweenShowDelay(object source, EventArgs e)
{
ResetCurrentToolTipTimer();
}
private ToolTip PendingToolTip
{
get { return _pendingToolTip; }
set { _pendingToolTip = value; }
}
private DispatcherTimer PendingToolTipTimer
{
get { return _pendingToolTipTimer; }
set { _pendingToolTipTimer = value; }
}
internal ToolTip CurrentToolTip
{
get { return _currentToolTip; }
}
private DispatcherTimer CurrentToolTipTimer
{
get { return _currentToolTipTimer; }
set { _currentToolTipTimer = value; }
}
private IInputElement LastMouseDirectlyOver
{
get { return _lastMouseDirectlyOver.GetValue(); }
set { _lastMouseDirectlyOver.SetValue(value); }
}
private DependencyObject LastMouseToolTipOwner
{
get { return _lastMouseToolTipOwner.GetValue(); }
set { _lastMouseToolTipOwner.SetValue(value); }
}
private DependencyObject GetOwner(ToolTip t)
{
return t?.GetValue(OwnerProperty) as DependencyObject;
}
// a pending request is represented by a sentinel ToolTip object that carries
// the owner and the trigger action (only). There's never more than one
// pending request, so we reuse the same sentinel object.
private ToolTip SentinelToolTip(DependencyObject o, ToolTipService.TriggerAction triggerAction)
{
// lazy creation, because we cannot create it in the ctor (infinite loop with FrameworkServices..ctor)
if (_sentinelToolTip == null)
{
_sentinelToolTip = new ToolTip();
}
_sentinelToolTip.SetValue(OwnerProperty, o);
_sentinelToolTip.FromKeyboard = ToolTipService.IsFromKeyboard(triggerAction);
return _sentinelToolTip;
}
#region Safe Area
private void SetSafeArea(ToolTip tooltip)
{
SafeArea = null; // default is no safe area
// safe area is only needed for tooltips triggered by mouse
if (tooltip != null && !tooltip.FromKeyboard)
{
DependencyObject owner = GetOwner(tooltip);
PresentationSource presentationSource = (owner != null) ? PresentationSource.CriticalFromVisual(owner) : null;
if (presentationSource != null)
{
// build a list of (native) rects, in the presentationSource's client coords
List<NativeMethods.RECT> rects = new List<NativeMethods.RECT>();
// add the owner rect(s)
UIElement ownerUIE;
ContentElement ownerCE;
if ((ownerUIE = owner as UIElement) != null)
{
// tooltip is owned by a UIElement.
Rect rectElement = new Rect(new Point(0, 0), ownerUIE.RenderSize);
Rect rectRoot = PointUtil.ElementToRoot(rectElement, ownerUIE, presentationSource);
Rect ownerRect = PointUtil.RootToClient(rectRoot, presentationSource);
if (!ownerRect.IsEmpty)
{
rects.Add(PointUtil.FromRect(ownerRect));
}
}
else if ((ownerCE = owner as ContentElement) != null)
{
// tooltip is owned by a ContentElement (e.g. Hyperlink).
IContentHost ichParent = null;
UIElement uieParent = KeyboardNavigation.GetParentUIElementFromContentElement(ownerCE, ref ichParent);
Visual visualParent = ichParent as Visual;
if (visualParent != null && uieParent != null)
{
IReadOnlyCollection<Rect> ownerRects = ichParent.GetRectangles(ownerCE);
// we're going to do the same transformations as in the UIElement case above.
// But using the PointUtil convenience methods would recompute transforms that
// are the same for each rect. Instead, do the usual optimization of computing
// common expressions before the loop, leaving only loop-dependent work inside.
GeneralTransform transformToRoot = visualParent.TransformToAncestor(presentationSource.RootVisual);
CompositionTarget target = presentationSource.CompositionTarget;
Matrix matrixRootTransform = PointUtil.GetVisualTransform(target.RootVisual);
Matrix matrixDPI = target.TransformToDevice;
foreach (Rect rect in ownerRects)
{
Rect rectRoot = transformToRoot.TransformBounds(rect);
Rect rectRootUntransformed = Rect.Transform(rectRoot, matrixRootTransform);
Rect rectClient = Rect.Transform(rectRootUntransformed, matrixDPI);
rects.Add(PointUtil.FromRect(rectClient));
}
}
}
// add the tooltip rect
Rect screenRect = tooltip.GetScreenRect();
Point clientPt = PointUtil.ScreenToClient(screenRect.Location, presentationSource);
Rect tooltipRect = new Rect(clientPt, screenRect.Size);
if (!tooltipRect.IsEmpty)
{
rects.Add(PointUtil.FromRect(tooltipRect));
}
// find the convex hull
SafeArea = new ConvexHull(presentationSource, rects);
}
}
}
private bool MouseHasLeftSafeArea()
{
// if there is no SafeArea, the mouse didn't leave it
if (SafeArea == null)
return false;
// if the current tooltip's owner is no longer being displayed, the safe area is no longer valid
// so the mouse has effectively left it
DependencyObject owner = GetOwner(CurrentToolTip);
PresentationSource presentationSource = (owner != null) ? PresentationSource.CriticalFromVisual(owner) : null;
if (presentationSource == null)
return true;
// if the safe area is valid, see if it still contains the mouse point
return !(SafeArea?.ContainsMousePoint() ?? true);
}
private ConvexHull SafeArea { get; set; }
#endregion
#endregion
#region ContextMenu
/// <summary>
/// Event that fires on ContextMenu when it opens.
/// Located here to avoid circular dependencies.
/// </summary>
internal static readonly RoutedEvent ContextMenuOpenedEvent =
EventManager.RegisterRoutedEvent("Opened", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PopupControlService));
/// <summary>
/// Event that fires on ContextMenu when it closes.
/// Located here to avoid circular dependencies.
/// </summary>
internal static readonly RoutedEvent ContextMenuClosedEvent =
EventManager.RegisterRoutedEvent("Closed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PopupControlService));
/////////////////////////////////////////////////////////////////////
private void RaiseContextMenuOpeningEvent(KeyEventArgs e)
{
IInputElement source = e.OriginalSource as IInputElement;
if (source != null)
{
if (RaiseContextMenuOpeningEvent(source, -1.0, -1.0,e.UserInitiated))
{
e.Handled = true;
}
}
}
private bool RaiseContextMenuOpeningEvent(IInputElement source, double x, double y,bool userInitiated)
{
// Fire the event
ContextMenuEventArgs args = new ContextMenuEventArgs(source, true /* opening */, x, y);
DependencyObject sourceDO = source as DependencyObject;
if (userInitiated && sourceDO != null)
{
if (sourceDO is UIElement uiElement)
{
uiElement.RaiseEvent(args, userInitiated);
}
else if (sourceDO is ContentElement contentElement)
{
contentElement.RaiseEvent(args, userInitiated);
}
else if (sourceDO is UIElement3D uiElement3D)
{
uiElement3D.RaiseEvent(args, userInitiated);
}
else
{
source.RaiseEvent(args);
}
}
else
{
source.RaiseEvent(args);
}
if (!args.Handled)
{
// No one handled the event, auto show any available ContextMenus
// Saved from the bubble up the tree where we looked for a set ContextMenu property
DependencyObject o = args.TargetElement;
if ((o != null) && ContextMenuService.ContextMenuIsEnabled(o))
{
// Retrieve the value
object menu = ContextMenuService.GetContextMenu(o);
ContextMenu cm = menu as ContextMenu;
cm.SetValue(OwnerProperty, o);
cm.Closed += new RoutedEventHandler(OnContextMenuClosed);
if ((x == -1.0) && (y == -1.0))
{
// We infer this to mean that the ContextMenu was opened with the keyboard
cm.Placement = PlacementMode.Center;
}
else
{
// If there is a CursorLeft and CursorTop, it was opened with the mouse.
cm.Placement = PlacementMode.MousePoint;
}
// Clear any open tooltips
DismissToolTips();
cm.SetCurrentValueInternal(ContextMenu.IsOpenProperty, BooleanBoxes.TrueBox);
return true; // A menu was opened
}
return false; // There was no menu to open
}
// Clear any open tooltips since someone else opened one
DismissToolTips();
return true; // The event was handled by someone else
}
private void OnContextMenuClosed(object source, RoutedEventArgs e)
{
ContextMenu cm = source as ContextMenu;
if (cm != null)
{
cm.Closed -= OnContextMenuClosed;
DependencyObject o = (DependencyObject)cm.GetValue(OwnerProperty);
if (o != null)
{
cm.ClearValue(OwnerProperty);
UIElement uie = GetTarget(o);
if (uie != null)
{
if (!IsPresentationSourceNull(uie))
{
IInputElement inputElement = (o is ContentElement || o is UIElement3D) ? (IInputElement)o : (IInputElement)uie;
ContextMenuEventArgs args = new ContextMenuEventArgs(inputElement, false /*opening */);
inputElement.RaiseEvent(args);
}
}
}
}
}
private static bool IsPresentationSourceNull(DependencyObject uie)
{
return PresentationSource.CriticalFromVisual(uie) == null;
}
#endregion
#region Helpers
internal static DependencyObject FindParent(DependencyObject o)
{
// see if o is a Visual or a Visual3D
DependencyObject v = o as Visual;
if (v == null)
{
v = o as Visual3D;
}
ContentElement ce = (v == null) ? o as ContentElement : null;
if (ce != null)
{
o = ContentOperations.GetParent(ce);
if (o != null)
{
return o;
}
else
{
FrameworkContentElement fce = ce as FrameworkContentElement;
if (fce != null)
{
return fce.Parent;
}
}
}
else if (v != null)
{
return VisualTreeHelper.GetParent(v);
}
return null;
}
internal static DependencyObject FindContentElementParent(ContentElement ce)
{
DependencyObject nearestVisual = null;
DependencyObject o = ce;
while (o != null)
{
nearestVisual = o as Visual;
if (nearestVisual != null)
{
break;
}
nearestVisual = o as Visual3D;
if (nearestVisual != null)
{
break;
}
ce = o as ContentElement;
if (ce != null)
{
o = ContentOperations.GetParent(ce);
if (o == null)
{
FrameworkContentElement fce = ce as FrameworkContentElement;
if (fce != null)
{
o = fce.Parent;
}
}
}
else
{
// This could be application.
break;
}
}
return nearestVisual;
}
internal static bool IsElementEnabled(DependencyObject o)
{
bool enabled = true;
UIElement uie = o as UIElement;
ContentElement ce = (uie == null) ? o as ContentElement : null;
UIElement3D uie3D = (uie == null && ce == null) ? o as UIElement3D : null;
if (uie != null)
{
enabled = uie.IsEnabled;
}
else if (ce != null)
{
enabled = ce.IsEnabled;
}
else if (uie3D != null)
{
enabled = uie3D.IsEnabled;
}
return enabled;
}
internal static PopupControlService Current
{
get
{
return FrameworkElement.PopupControlService;
}
}
/// <summary>
/// Returns the UIElement target
/// </summary>
private static UIElement GetTarget(DependencyObject o)
{
UIElement uie = o as UIElement;
if (uie == null)
{
ContentElement ce = o as ContentElement;
if (ce != null)
{
DependencyObject ceParent = FindContentElementParent(ce);
// attempt to cast to a UIElement
uie = ceParent as UIElement;
if (uie == null)
{
// target can't be a UIElement3D - so get the nearest containing UIElement
UIElement3D uie3D = ceParent as UIElement3D;
if (uie3D != null)
{
uie = UIElementHelper.GetContainingUIElement2D(uie3D);
}
}
}
else
{
// it wasn't a UIElement or ContentElement, try one last cast to UIElement3D
// target can't be a UIElement3D - so get the nearest containing UIElement
UIElement3D uie3D = o as UIElement3D;
if (uie3D != null)
{
uie = UIElementHelper.GetContainingUIElement2D(uie3D);
}
}
}
return uie;
}
/// <summary>
/// Indicates whether the service owns the tooltip
/// </summary>
internal static readonly DependencyProperty ServiceOwnedProperty =
DependencyProperty.RegisterAttached("ServiceOwned", // Name
typeof(bool), // Type
typeof(PopupControlService), // Owner
new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));
/// <summary>
/// Stores the original element on which to fire the closed event
/// </summary>
internal static readonly DependencyProperty OwnerProperty =
DependencyProperty.RegisterAttached("Owner", // Name
typeof(DependencyObject), // Type
typeof(PopupControlService), // Owner
new FrameworkPropertyMetadata((DependencyObject)null, // Default Value
new PropertyChangedCallback(OnOwnerChanged)));
// When the owner changes, coerce all attached properties from the service
private static void OnOwnerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is ContextMenu)
{
o.CoerceValue(ContextMenu.HorizontalOffsetProperty);
o.CoerceValue(ContextMenu.VerticalOffsetProperty);
o.CoerceValue(ContextMenu.PlacementTargetProperty);
o.CoerceValue(ContextMenu.PlacementRectangleProperty);
o.CoerceValue(ContextMenu.PlacementProperty);
o.CoerceValue(ContextMenu.HasDropShadowProperty);
}
else if (o is ToolTip)
{
o.CoerceValue(ToolTip.HorizontalOffsetProperty);
o.CoerceValue(ToolTip.VerticalOffsetProperty);
o.CoerceValue(ToolTip.PlacementTargetProperty);
o.CoerceValue(ToolTip.PlacementRectangleProperty);
o.CoerceValue(ToolTip.PlacementProperty);
o.CoerceValue(ToolTip.HasDropShadowProperty);
}
}
// Returns the value of dp on the Owner if it is set there,
// otherwise returns the value set on o (the tooltip or contextmenu)
internal static object CoerceProperty(DependencyObject o, object value, DependencyProperty dp)
{
DependencyObject owner = (DependencyObject)o.GetValue(OwnerProperty);
if (owner != null)
{
bool hasModifiers;
if (owner.GetValueSource(dp, null, out hasModifiers) != BaseValueSourceInternal.Default || hasModifiers)
{
// Return a value if it is set on the owner
return owner.GetValue(dp);
}
else if (dp == ToolTip.PlacementTargetProperty || dp == ContextMenu.PlacementTargetProperty)
{
UIElement uie = GetTarget(owner);
// If it is the PlacementTarget property, return the owner itself
if (uie != null)
return uie;
}
}
return value;
}
#endregion
#region Private Types
struct WeakRefWrapper<T> where T : class
{
private WeakReference<T> _storage;
public T GetValue()
{
T value;
if (_storage != null)
{
if (!_storage.TryGetTarget(out value))
{
_storage = null;
}
}
else
{
value = null;
}
return value;
}
public void SetValue(T value)
{
if (value == null)
{
_storage = null;
}
else if (_storage == null)
{
_storage = new WeakReference<T>(value);
}
else
{
_storage.SetTarget(value);
}
}
}
// A region is convex if every line segment connecting two points of the region lies
// within the region. The convex hull of a set of points is the smallest convex region
// that contains the points. This is just what we need for the safe area of a tooltip
// and its owner: the tooltip should remain open as long as the mouse lies on a line
// segment connecting some point in the owner to some point in the tooltip, i.e. as long
// as the mouse is in the convex hull of the corners of the owner and tooltip rectangles.
//
// There are several aspects of this use-case we can exploit.
// * The points come from WM_MOUSEMOVE messages, in the coords of the hwnd's client area.
// This means they are 16-bit integers. We can compute cross-products using
// integer multiplication without fear of overflow.
// * The convex hull is built from only 8 points - the corners of the two rectangles.
// We can use simple algorithms with low overhead, ignoring their less-than-optimal
// asymptotic cost.
// * The convex hull will have between 4 and 8 edges, at least 4 of which are axis-aligned.
// We can test these edges by simple integer comparison, no multiplications needed.
//
// These remarks apply to the case when the tooltip owner is a UIElement, and thus has a
// single bounding rectangle. When the owner is a ContentElement, it's bounding area can
// be the union of many rectangles. Nevertheless, the remarks still apply qualitatively:
// the top-down scan is still efficient in practice (the rectangles usually arrive in
// top-down order already), and the majority of edges in the resulting convex hull are
// axis-aligned.
class ConvexHull
{
internal ConvexHull(PresentationSource source, List<NativeMethods.RECT> rects)
{
_source = source;
PointList points = new PointList();
if (rects.Count == 1)
{
// special-case optimization: the hull of a single rectangle is the rectangle itself
AddPoints(points, rects[0], rectIsHull: true);
_points = points.ToArray();
}
else
{
foreach (NativeMethods.RECT rect in rects)
{
AddPoints(points, rect);
}
SortPoints(points);
BuildHullIncrementally(points);
}
}
// sort by y (and by x among equal y's)
private void SortPoints(PointList points)
{
// insertion sort is good enough. We're dealing with a small
// set of points that are nearly in the right order already.
for (int i=1, N=points.Count; i<N; ++i)
{
Point p = points[i];
int j;
for (j=i-1; j>=0; --j)
{
int d = points[j].Y - p.Y;
if (d > 0 || (d == 0 && (points[j].X > p.X)))
{
points[j + 1] = points[j];
}
else break;
}
points[j + 1] = p;
}
}
// build the convex hull
// Precondition: the points are sorted, in the sense of SortPoints
private void BuildHullIncrementally(PointList points)
{
int N = points.Count;
int currentIndex = 0;
int hullCount = 0;
int prevLeftmostIndex = 0, prevRightmostIndex = 0;
// loop invariant:
// * given a value Y = points[currentIndex].Y, partition
// the original points into two sets: a "small" set - points
// whose y < Y, and a "large" set - points whose y >= Y
// * the first hullCount points, points[0 ... hullCount-1], are the
// convex hull (in counterclockwise order) of the small points
// * the large points are in their original positions in
// points[currentIndex ... N-1], and haven't been examined.
while (currentIndex < N)
{
// Each iteration will deal with all the points whose y == Y,
// incrementally extending the convex hull to include them.
int Y = points[currentIndex].Y;
// find the leftmost and rightmost points whose y == Y
// (given that the points are sorted, these are simply the
// first and last points whose y == Y)
Point leftmost = points[currentIndex];
int next = currentIndex + 1;
while (next<N && points[next].Y == Y)
{
++next;
}
Point rightmost = points[next - 1];
// remember if these are the same point, and advance currentIndex
// past the points whose y == Y
int pointsToAdd = (next == currentIndex + 1) ? 1 : 2;
currentIndex = next;
// add these point(s) to the partial convex hull
if (hullCount == 0)
{
// the first iteration is special: there are no points
// to remove, and we have to add the new points in the
// opposite order to get "counterclockwise" correct.
if (pointsToAdd == 2)
{
points[0] = rightmost;
points[1] = leftmost;
prevLeftmostIndex = 1;
}
else
{
points[0] = leftmost;
prevLeftmostIndex = 0;
}
prevRightmostIndex = hullCount = pointsToAdd;
}
else
{
// in the remaining iterations, the new point(s) replace
// a (possibly empty) segment of the current hull. To
// identify that segment, locate the two points on the
// current convex hull that have the minimum polar angle with
// leftmost, and the maximum polar angle with rightmost.
// (It's possible to use binary search for this, but that
// adds overhead that wouldn't pay off in our small scenarios.)
// First examine the points in clockwise order, starting with the
// previous iteration's leftmost point. The polar angle with
// leftmost will decrease for a while, then increase. The first
// increase (or termination) occurs at the desired minimum.
int minIndex = prevLeftmostIndex;
for (; minIndex > 0; --minIndex)
{
if (Cross(leftmost, points[minIndex], points[minIndex - 1]) > 0)
break;
}
// Similarly, examine the points in counterclockwise order, starting
// with the previous iteration's rightmost point. The polar angle
// with rightmost will increase for a while, and the first decrease
// occurs at the desired maximum.
int maxIndex = prevRightmostIndex;
for (; maxIndex < hullCount; ++maxIndex)
{
int wrapIndex = maxIndex + 1;
if (wrapIndex == hullCount) wrapIndex = 0;
if (Cross(rightmost, points[maxIndex], points[wrapIndex]) < 0)
break;
}
// replace the segment of the hull between these two points with
// the leftmost and rightmost point(s)
int pointsToRemove = maxIndex - minIndex - 1;
int delta = pointsToAdd - pointsToRemove;
// move retained points to their new position
// (the hull is a subset of the original points, which
// guarantees that the indices into points are
// always in bounds).
if (delta < 0)
{
for (int i=maxIndex; i<hullCount; ++i)
{
points[i + delta] = points[i];
}
}
else if (delta > 0)
{
for (int i=hullCount-1; i>=maxIndex; --i)
{
points[i + delta] = points[i];
}
}
// insert the new point(s), and update the hull size
points[minIndex + 1] = leftmost;
prevLeftmostIndex = prevRightmostIndex = minIndex + 1;
if (pointsToAdd == 2)
{
points[minIndex + 2] = rightmost;
prevRightmostIndex = minIndex + 2;
}
hullCount += delta;
}
}
// when the loop terminates, the loop invariant plus the condition
// (currentIndex >= N) imply points[0 ... hullCount-1] describe the
// convex hull of the original points. All that's left is to discard
// any extra points, and compute the directions.
points.RemoveRange(hullCount, N - hullCount);
_points = points.ToArray();
SetDirections();
}
// set the Direction field on each point. This enables optimizations during
// ContainsPoint.
private void SetDirections()
{
for (int i=0, N=_points.Length; i<N; ++i)
{
int next = i + 1;
if (next == N) next = 0;
if (_points[i].X == _points[next].X)
{
_points[i].Direction = (_points[i].Y >= _points[next].Y) ? Direction.Up : Direction.Down;
}
else if (_points[i].Y == _points[next].Y)
{
_points[i].Direction = (_points[i].X >= _points[next].X) ? Direction.Left : Direction.Right;
}
else
{
_points[i].Direction = Direction.Skew;
}
}
}
private void AddPoints(PointList points, in NativeMethods.RECT rect, bool rectIsHull=false)
{
if (rectIsHull)
{
// caller is asserting the convex hull is the rect itself,
// add its corner points in counterclockwise order with directions set
points.Add(new Point(rect.right, rect.top, Direction.Left));
points.Add(new Point(rect.left, rect.top, Direction.Down));
points.Add(new Point(rect.left, rect.bottom, Direction.Right));
points.Add(new Point(rect.right, rect.bottom, Direction.Up));
}
else
{
// otherwise add the corner points in an order favorable to SortPoints
points.Add(new Point(rect.left, rect.top));
points.Add(new Point(rect.right, rect.top));
points.Add(new Point(rect.left, rect.bottom));
points.Add(new Point(rect.right, rect.bottom));
}
}
// Test whether the current mouse point lies within the convex hull
internal bool ContainsMousePoint()
{
// get the coordinates of the current mouse point, relative to the Active source
PresentationSource mouseSource = Mouse.PrimaryDevice.CriticalActiveSource;
System.Windows.Point pt = Mouse.PrimaryDevice.NonRelativePosition;
// translate the point to our source's coordinates, if necessary
// (e.g. if the tooltip's owner comes from a window with capture,
// such as the popup of a ComboBox)
if (mouseSource != _source)
{
System.Windows.Point ptScreen = PointUtil.ClientToScreen(pt, mouseSource);
pt = PointUtil.ScreenToClient(ptScreen, _source);
}
#if DEBUG
// NonRelativePosition returns the mouse point in unscaled screen coords, relative
// to the active window's client area (despite the name).
// Compute the point a different way, and check that it agrees. The second
// way uses public API, but in our case ends up doing a lot of transforms
// and multiplications that should simply cancel each other out.
System.Windows.Interop.HwndSource hwndSource = _source as System.Windows.Interop.HwndSource;
IInputElement rootElement = hwndSource?.RootVisual as IInputElement;
Debug.Assert(hwndSource != null && rootElement != null, "expect non-null hwndSource and rootElement");
System.Windows.Point pt2 = hwndSource.TransformToDevice(Mouse.PrimaryDevice.GetPosition(rootElement));
Debug.Assert(((int)pt.X == (int)Math.Round(pt2.X)) && ((int)pt.Y == (int)Math.Round(pt2.Y)), "got incorrect mouse point");
#endif
// check whether the point lies within the hull
return ContainsPoint(_source, (int)pt.X, (int)pt.Y);
// NOTE: NonRelativePosition doesn't actually return the position of the current mouse point,
// but rather the last recorded position. (See MouseDevice.GetScreenPositionFromSystem,
// which says that "Win32 has issues reliably returning where the mouse is".)
// This causes a small problem when (a) the PresentationSource has capture, e.g.
// the popup of a ComboBox, and (b) the mouse moves to a position that lies outside both the
// capturing PresentationSource (popup window) and the input-providing PresentationSource
// (main window). The MouseDevice only records positions within the input-providing
// PresentationSource, so we'll test the position where the mouse left the main window,
// rather than the current position.
// This means we may leave a tooltip open even when the mouse leaves its SafeArea,
// but only when the tooltip belongs to a capturing source, and the "leaving the SafeArea"
// action occurs outside the surrounding main window. For our example, it can happen
// when the ComboBox is close to the edge of the main window so that a tooltip from its
// popup content extends beyond the main window.
// This can only be fixed by changing MouseDevice.GetScreenPositionFromSystem to
// use a "better way" to find the current mouse position, which allegedly needs work from the OS.
// But we can live with this behavior, because
// * this is a corner case - tooltips from popup content that extend beyond the main window
// * the effect is transient - the tooltip will close when the user dismisses the popup
// * there's no accessibility issue - WCAG 2.1 only requires that the tooltip stays open under
// proscribed conditions, not that it has to close when the conditions cease to apply
}
// Test whether a given mouse point (x,y) lies within the convex hull
internal bool ContainsPoint(PresentationSource source, int x, int y)
{
// points from the wrong source are not included
if (source != _source)
return false;
// a point is included if it's in the left half-plane of every
// edge. We test this in two passes, to postpone (and perhaps
// avoid) multiplications, and to get the customary "exclusive"
// behavior for edges that came from the bottom or right edges
// of the original rectangles.
// Pass 1 - handle the axis-aligned edges
for (int i = 0, N = _points.Length; i < N; ++i)
{
switch (_points[i].Direction)
{
case Direction.Left:
if (y < _points[i].Y) return false;
break;
case Direction.Right:
if (y >= _points[i].Y) return false;
break;
case Direction.Up:
if (x >= _points[i].X) return false;
break;
case Direction.Down:
if (x < _points[i].X) return false;
break;
}
}
// Pass 2 - handle the skew edges
for (int i = 0, N = _points.Length; i < N; ++i)
{
switch (_points[i].Direction)
{
case Direction.Skew:
int next = i + 1;
if (next == N) next = 0;
Point p = new Point(x, y);
if (Cross(_points[i], _points[next], p) > 0)
return false;
break;
}
}
// the point is on the correct side of all the edges
return true;
}
// returns c's position relative to the line extending segment a -> b:
// <0 if c is in the left half-plane
// 0 if c is on the line
// >0 if c is in the right half-plane
private static int Cross(in Point a, in Point b, in Point c)
{
return (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
}
enum Direction { Skew, Left, Right, Up, Down }
[DebuggerDisplay("{X} {Y} {Direction}")]
struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Direction Direction { get; set; }
public Point(int x, int y, Direction d=Direction.Skew)
{
X = x;
Y = y;
Direction = d;
}
}
class PointList : List<Point>
{ }
Point[] _points;
PresentationSource _source;
}
#endregion
#region Data
// see comment in BeginShowTooltip. This should be large enough to
// allow continuous mouse-move events, but small enough to switch
// tooltips instantly (where "continuous" and "instantly" are the
// end-user's perception). The value here is large enough to make the
// "SafeAreaOnHyperlink" test pass.
static private int ShortDelay = 73;
// pending ToolTip
private ToolTip _pendingToolTip;
private DispatcherTimer _pendingToolTipTimer;
private ToolTip _sentinelToolTip;
// current ToolTip
private ToolTip _currentToolTip;
private DispatcherTimer _currentToolTipTimer;
private DispatcherTimer _forceCloseTimer;
private Key _lastCtrlKeyDown;
// ToolTip history
private WeakRefWrapper<IInputElement> _lastMouseDirectlyOver;
private WeakRefWrapper<DependencyObject> _lastMouseToolTipOwner;
private bool _quickShow = false; // true if a tool tip closed recently
#endregion
}
}
|