1 //----------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 //----------------------------------------------------------------
4 namespace System.Activities.Presentation.Internal.PropertyEditing.Selection
7 using System.ComponentModel;
8 using System.Diagnostics;
9 using System.Diagnostics.CodeAnalysis;
11 using System.Windows.Input;
12 using System.Windows.Media;
15 using System.Activities.Presentation.Internal.PropertyEditing.Selection;
16 using System.Activities.Presentation;
19 // This is a container for attached properties used by PropertyInspector to track and manage
20 // property selection. It is public because WPF requires that attached properties used in XAML
21 // be declared by public classes.
23 [EditorBrowsable(EditorBrowsableState.Never)]
24 static class PropertySelection
27 private static readonly DependencyPropertyKey IsSelectedPropertyKey = DependencyProperty.RegisterAttachedReadOnly(
30 typeof(PropertySelection),
31 new PropertyMetadata(false));
34 // Attached, ReadOnly DP that we use to mark objects as selected. If they care, they can then render
35 // themselves differently.
37 internal static readonly DependencyProperty IsSelectedProperty = IsSelectedPropertyKey.DependencyProperty;
40 // Attached DP that we use in XAML to mark elements that can be selected.
42 internal static readonly DependencyProperty SelectionStopProperty = DependencyProperty.RegisterAttached(
44 typeof(ISelectionStop),
45 typeof(PropertySelection),
46 new PropertyMetadata(null));
49 // Attached DP used in conjunction with SelectionStop DP. It specifies the FrameworkElement to hook into
50 // in order to handle double-click events to control the expanded / collapsed state of its parent SelectionStop.
52 internal static readonly DependencyProperty IsSelectionStopDoubleClickTargetProperty = DependencyProperty.RegisterAttached(
53 "IsSelectionStopDoubleClickTarget",
55 typeof(PropertySelection),
56 new PropertyMetadata(false, new PropertyChangedCallback(OnIsSelectionStopDoubleClickTargetChanged)));
59 // Attached DP that we use in XAML to mark elements as selection scopes - meaning selection
60 // won't spill beyond the scope of the marked element.
62 internal static readonly DependencyProperty IsSelectionScopeProperty = DependencyProperty.RegisterAttached(
65 typeof(PropertySelection),
66 new PropertyMetadata(false));
69 // Attached property we use to route non-navigational key strokes from one FrameworkElement to
70 // another. When this property is set on a FrameworkElement, we hook into its KeyDown event
71 // and send any unhandled, non-navigational key strokes to the FrameworkElement specified
72 // by this property. The target FrameworkElement must be focusable or have a focusable child.
73 // When the first eligible key stroke is detected, the focus will be shifted to the focusable
74 // element and the key stroke will be sent to it.
76 internal static readonly DependencyProperty KeyDownTargetProperty = DependencyProperty.RegisterAttached(
78 typeof(FrameworkElement),
79 typeof(PropertySelection),
80 new PropertyMetadata(null, new PropertyChangedCallback(OnKeyDownTargetChanged)));
82 // Constant that determines how deep in the visual tree we search for SelectionStops that
83 // are children or neighbors of a given element (usually one that the user clicked on) before
84 // giving up. This constant is UI-dependent.
85 private const int MaxSearchDepth = 11;
88 // Gets PropertySelection.IsSelected property from the specified DependencyObject
90 // <param name="obj">DependencyObject to examine</param>
91 // <returns>Value of the IsSelected property</returns>
92 internal static bool GetIsSelected(DependencyObject obj)
96 throw FxTrace.Exception.ArgumentNull("obj");
99 return (bool)obj.GetValue(IsSelectedProperty);
102 // Private (internal) setter that we use to mark objects as selected from within CategoryList class
104 internal static void SetIsSelected(DependencyObject obj, bool value)
108 throw FxTrace.Exception.ArgumentNull("obj");
111 obj.SetValue(IsSelectedPropertyKey, value);
115 // SelectionStop Attached DP
118 // Gets PropertySelection.SelectionStop property from the specified DependencyObject
120 // <param name="obj">DependencyObject to examine</param>
121 // <returns>Value of the SelectionStop property.</returns>
122 internal static ISelectionStop GetSelectionStop(DependencyObject obj)
126 throw FxTrace.Exception.ArgumentNull("obj");
129 return (ISelectionStop)obj.GetValue(SelectionStopProperty);
133 // Sets PropertySelection.SelectionStop property on the specified DependencyObject
135 // <param name="obj">DependencyObject to modify</param>
136 // <param name="value">New value of SelectionStop</param>
137 internal static void SetSelectionStop(DependencyObject obj, ISelectionStop value)
141 throw FxTrace.Exception.ArgumentNull("obj");
144 obj.SetValue(SelectionStopProperty, value);
148 // Clears PropertySelection.SelectionStop property from the specified DependencyObject
150 // <param name="obj">DependencyObject to clear</param>
151 internal static void ClearSelectionStop(DependencyObject obj)
155 throw FxTrace.Exception.ArgumentNull("obj");
158 obj.ClearValue(SelectionStopProperty);
162 // IsSelectionStopDoubleClickTarget Attached DP
165 // Gets PropertySelection.IsSelectionStopDoubleClickTarget property from the specified DependencyObject
167 // <param name="obj">DependencyObject to examine</param>
168 // <returns>Value of the IsSelectionStopDoubleClickTarget property.</returns>
169 internal static bool GetIsSelectionStopDoubleClickTarget(DependencyObject obj)
173 throw FxTrace.Exception.ArgumentNull("obj");
176 return (bool)obj.GetValue(IsSelectionStopDoubleClickTargetProperty);
180 // Sets PropertySelection.IsSelectionStopDoubleClickTarget property on the specified DependencyObject
182 // <param name="obj">DependencyObject to modify</param>
183 // <param name="value">New value of IsSelectionStopDoubleClickTarget</param>
184 internal static void SetIsSelectionStopDoubleClickTarget(DependencyObject obj, bool value)
188 throw FxTrace.Exception.ArgumentNull("obj");
191 obj.SetValue(IsSelectionStopDoubleClickTargetProperty, value);
195 // Clears PropertySelection.IsSelectionStopDoubleClickTarget property from the specified DependencyObject
197 // <param name="obj">DependencyObject to modify</param>
198 internal static void ClearIsSelectionStopDoubleClickTarget(DependencyObject obj)
202 throw FxTrace.Exception.ArgumentNull("obj");
205 obj.ClearValue(IsSelectionStopDoubleClickTargetProperty);
208 // Called when some object gets specified as the SelectionStop double-click target:
210 // * Hook into the MouseDown event so that we can detect double-clicks and automatically
211 // expand or collapse the corresponding SelectionStop, if possible
213 private static void OnIsSelectionStopDoubleClickTargetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
215 FrameworkElement target = sender as FrameworkElement;
221 if (bool.Equals(e.OldValue, false) && bool.Equals(e.NewValue, true))
223 AddDoubleClickHandler(target);
225 else if (bool.Equals(e.OldValue, true) && bool.Equals(e.NewValue, false))
227 RemoveDoubleClickHandler(target);
231 // Called when some SelectionStop double-click target gets unloaded:
233 // * Unhook from events so that we don't prevent garbage collection
235 private static void OnSelectionStopDoubleClickTargetUnloaded(object sender, RoutedEventArgs e)
237 FrameworkElement target = sender as FrameworkElement;
238 Fx.Assert(target != null, "sender parameter should not be null");
245 RemoveDoubleClickHandler(target);
248 // Called when the UI object representing a SelectionStop gets clicked:
250 // * If this is a double-click and the SelectionStop can be expanded / collapsed,
251 // expand / collapse the SelectionStop
253 private static void OnSelectionStopDoubleClickTargetMouseDown(object sender, MouseButtonEventArgs e)
255 DependencyObject target = e.OriginalSource as DependencyObject;
261 if (e.ClickCount > 1)
264 FrameworkElement parentSelectionStopVisual = PropertySelection.FindParentSelectionStop<FrameworkElement>(target);
265 if (parentSelectionStopVisual != null)
268 ISelectionStop parentSelectionStop = PropertySelection.GetSelectionStop(parentSelectionStopVisual);
269 if (parentSelectionStop != null && parentSelectionStop.IsExpandable)
271 parentSelectionStop.IsExpanded = !parentSelectionStop.IsExpanded;
277 private static void AddDoubleClickHandler(FrameworkElement target)
279 target.AddHandler(UIElement.MouseDownEvent, new MouseButtonEventHandler(OnSelectionStopDoubleClickTargetMouseDown), false);
280 target.Unloaded += new RoutedEventHandler(OnSelectionStopDoubleClickTargetUnloaded);
283 private static void RemoveDoubleClickHandler(FrameworkElement target)
285 target.Unloaded -= new RoutedEventHandler(OnSelectionStopDoubleClickTargetUnloaded);
286 target.RemoveHandler(UIElement.MouseDownEvent, new MouseButtonEventHandler(OnSelectionStopDoubleClickTargetMouseDown));
290 // IsSelectionScope Attached DP
293 // Gets PropertySelection.IsSelectionScope property from the specified DependencyObject
295 // <param name="obj">DependencyObject to examine</param>
296 // <returns>Value of the IsSelectionScope property.</returns>
297 internal static bool GetIsSelectionScope(DependencyObject obj)
301 throw FxTrace.Exception.ArgumentNull("obj");
304 return (bool)obj.GetValue(IsSelectionScopeProperty);
308 // Sets PropertySelection.IsSelectionScope property on the specified DependencyObject
310 // <param name="obj">DependencyObject to modify</param>
311 // <param name="value">New value of IsSelectionScope</param>
312 internal static void SetIsSelectionScope(DependencyObject obj, bool value)
316 throw FxTrace.Exception.ArgumentNull("obj");
319 obj.SetValue(IsSelectionScopeProperty, value);
322 // KeyDownTarget Attached DP
325 // Gets PropertySelection.KeyDownTarget property from the specified DependencyObject
327 // <param name="obj">DependencyObject to examine</param>
328 // <returns>Value of the KeyDownTarget property.</returns>
329 internal static FrameworkElement GetKeyDownTarget(DependencyObject obj)
333 throw FxTrace.Exception.ArgumentNull("obj");
336 return (FrameworkElement)obj.GetValue(KeyDownTargetProperty);
340 // Sets PropertySelection.KeyDownTarget property on the specified DependencyObject
342 // <param name="obj">DependencyObject to modify</param>
343 // <param name="value">New value of KeyDownTarget</param>
344 internal static void SetKeyDownTarget(DependencyObject obj, FrameworkElement value)
348 throw FxTrace.Exception.ArgumentNull("obj");
351 obj.SetValue(KeyDownTargetProperty, value);
354 // Called when some FrameworkElement gets specified as the target for KeyDown RoutedEvents -
355 // hook into / unhook from the KeyDown event of the source
356 private static void OnKeyDownTargetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
358 FrameworkElement target = sender as FrameworkElement;
364 if (e.OldValue != null && e.NewValue == null)
366 RemoveKeyStrokeHandlers(target);
368 else if (e.NewValue != null && e.OldValue == null)
370 AddKeyStrokeHandlers(target);
374 // Called when a KeyDownTarget gets unloaded -
375 // unhook from events so that we don't prevent garbage collection
376 private static void OnKeyDownTargetUnloaded(object sender, RoutedEventArgs e)
378 FrameworkElement target = sender as FrameworkElement;
379 Fx.Assert(target != null, "sender parameter should not be null");
386 RemoveKeyStrokeHandlers(target);
389 // Called when a KeyDownTarget is specified and a KeyDown event is detected on the source
390 private static void OnKeyDownTargetKeyDown(object sender, KeyEventArgs e)
393 // Ignore handled events
399 // Ignore navigation keys
400 if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up || e.Key == Key.Down ||
401 e.Key == Key.Tab || e.Key == Key.Escape || e.Key == Key.Return || e.Key == Key.Enter ||
402 e.Key == Key.PageUp || e.Key == Key.PageDown || e.Key == Key.Home || e.Key == Key.End || e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl)
407 if (Keyboard.Modifiers == ModifierKeys.Control)
414 DependencyObject keySender = sender as DependencyObject;
415 Fx.Assert(keySender != null, "keySender should not be null");
416 if (keySender == null)
421 FrameworkElement keyTarget = GetKeyDownTarget(keySender);
422 Fx.Assert(keyTarget != null, "keyTarget should not be null");
423 if (keyTarget == null)
428 // Find a focusable element on the target, set focus to it, and send the keys over
429 FrameworkElement focusable = VisualTreeUtils.FindFocusableElement<FrameworkElement>(keyTarget);
430 if (focusable != null && focusable == Keyboard.Focus(focusable))
432 focusable.RaiseEvent(e);
436 private static void AddKeyStrokeHandlers(FrameworkElement target)
438 target.AddHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnKeyDownTargetKeyDown), false);
439 target.Unloaded += new RoutedEventHandler(OnKeyDownTargetUnloaded);
442 private static void RemoveKeyStrokeHandlers(FrameworkElement target)
444 target.Unloaded -= new RoutedEventHandler(OnKeyDownTargetUnloaded);
445 target.RemoveHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnKeyDownTargetKeyDown));
450 // Returns the closest parent (or the element itself) marked as a SelectionStop.
452 // <typeparam name="T">Type of element to look for</typeparam>
453 // <param name="element">Element to examine</param>
454 // <returns>The closest parent (or the element itself) marked as a SelectionStop;
455 // null if not found.</returns>
456 internal static T FindParentSelectionStop<T>(DependencyObject element) where T : DependencyObject
465 // IsEligibleSelectionStop already checks for visibility, so we don't need to
466 // to do a specific check somewhere else in this loop
467 if (IsEligibleSelectionStop<T>(element))
472 element = VisualTreeHelper.GetParent(element);
473 } while (element != null);
479 // Returns the closest neighbor in the given direction marked as a SelectionStop.
481 // <typeparam name="T">Type of element to look for</typeparam>
482 // <param name="element">Element to examine</param>
483 // <param name="direction">Direction to search in</param>
484 // <returns>The closest neighboring element in the given direction marked as a IsSelectionStop,
485 // if found, null otherwise.</returns>
486 internal static T FindNeighborSelectionStop<T>(DependencyObject element, SearchDirection direction) where T : DependencyObject
491 throw FxTrace.Exception.ArgumentNull("element");
495 int maxSearchDepth = MaxSearchDepth;
497 // If we are looking for the NEXT element and we can dig deeper, start by digging deeper
498 // before trying to look for any siblings.
500 if (direction == SearchDirection.Next && IsExpanded(element))
502 neighbor = FindChildSelectionStop<T>(element, 0, VisualTreeHelper.GetChildrenCount(element) - 1, direction, maxSearchDepth, MatchDirection.Down);
504 if (neighbor != null)
510 int childIndex, childrenCount, childDepth;
511 bool isParentSelectionStop, isParentSelectionScope = false;
512 DependencyObject parent = element;
518 // If we reached the selection scope, don't try to go beyond it
519 if (isParentSelectionScope)
524 parent = GetEligibleParent(parent, out childIndex, out childrenCount, out childDepth, out isParentSelectionStop, out isParentSelectionScope);
525 maxSearchDepth += childDepth;
532 if (direction == SearchDirection.Next && (childIndex + 1) >= childrenCount)
537 if (direction == SearchDirection.Previous && isParentSelectionStop == false && (childIndex < 1))
545 // If we get here, that means we found a SelectionStop on which we need to look for children that are
546 // SelectionStops themselves. The first such child found should be returned. Otherwise, if no such child
547 // is found, we potentially look at the node itself and return it OR we repeat the process and keep looking
548 // for a better parent.
550 int leftIndex, rightIndex;
551 MatchDirection matchDirection;
553 if (direction == SearchDirection.Previous)
556 rightIndex = childIndex - 1;
557 matchDirection = MatchDirection.Up;
561 leftIndex = childIndex + 1;
562 rightIndex = childrenCount - 1;
563 matchDirection = MatchDirection.Down;
566 neighbor = FindChildSelectionStop<T>(parent, leftIndex, rightIndex, direction, maxSearchDepth, matchDirection);
567 if (neighbor != null)
572 if (direction == SearchDirection.Previous &&
573 IsEligibleSelectionStop<T>(parent))
580 // Helper method used from GetNeighborSelectionStop()
581 // Returns a parent DependencyObject of the specified element that is
584 // * ( Marked with a SelectionStop OR
585 // * Marked with IsSelectionScope = true OR
586 // * Has more than one child )
588 private static DependencyObject GetEligibleParent(DependencyObject element, out int childIndex, out int childrenCount, out int childDepth, out bool isSelectionStop, out bool isSelectionScope)
591 isSelectionStop = false;
592 isSelectionScope = false;
597 element = VisualTreeUtils.GetIndexedVisualParent(element, out childrenCount, out childIndex);
598 isSelectionStop = element == null ? false : (GetSelectionStop(element) != null);
599 isSelectionScope = element == null ? false : GetIsSelectionScope(element);
600 isVisible = VisualTreeUtils.IsVisible(element as UIElement);
606 (isVisible == false ||
607 (isSelectionStop == false &&
608 isSelectionScope == false &&
609 childrenCount < 2)));
614 // Helper method that performs a recursive, depth-first search of children starting at the specified parent,
615 // looking for any children that conform to the specified Type and are marked with a SelectionStop
617 private static T FindChildSelectionStop<T>(DependencyObject parent, int leftIndex, int rightIndex, SearchDirection iterationDirection, int maxDepth, MatchDirection matchDirection)
618 where T : DependencyObject
621 if (parent == null || maxDepth <= 0)
626 int step = iterationDirection == SearchDirection.Next ? 1 : -1;
627 int index = iterationDirection == SearchDirection.Next ? leftIndex : rightIndex;
629 for (; index >= leftIndex && index <= rightIndex; index = index + step)
632 DependencyObject child = VisualTreeHelper.GetChild(parent, index);
634 // If MatchDirection is set to Down, do an eligibility match BEFORE we dive down into
637 if (matchDirection == MatchDirection.Down && IsEligibleSelectionStop<T>(child))
642 // If this child is not an eligible SelectionStop because it is not visible,
643 // there is no point digging down to get to more children.
645 if (!VisualTreeUtils.IsVisible(child as UIElement))
650 int grandChildrenCount = VisualTreeHelper.GetChildrenCount(child);
651 if (grandChildrenCount > 0 && IsExpanded(child))
653 T element = FindChildSelectionStop<T>(child, 0, grandChildrenCount - 1, iterationDirection, maxDepth - 1, matchDirection);
661 // If MatchDirection is set to Up, do an eligibility match AFTER we tried diving into
662 // more children and failed to find something we could return.
664 if (matchDirection == MatchDirection.Up && IsEligibleSelectionStop<T>(child))
673 // Helper method that returns false if the given element is a collapsed SelectionStop,
676 private static bool IsExpanded(DependencyObject element)
678 ISelectionStop selectionStop = PropertySelection.GetSelectionStop(element);
679 return selectionStop == null || selectionStop.IsExpanded;
682 // Helper method that return true if the given element is marked with a SelectionStop,
683 // if it derives from the specified Type, and if it is Visible (assuming it derives from UIElement)
685 private static bool IsEligibleSelectionStop<T>(DependencyObject element) where T : DependencyObject
687 return (GetSelectionStop(element) != null) && typeof(T).IsAssignableFrom(element.GetType()) && VisualTreeUtils.IsVisible(element as UIElement);
691 // Private enum we use to specify whether FindSelectionStopChild() should return any matches
692 // as it drills down into the visual tree (Down) or whether it should wait on looking at
693 // matches until it's bubbling back up again (Up).
695 private enum MatchDirection
701 // IsSelected ReadOnly, Attached DP