//---------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------- namespace System.Activities.Presentation.Internal.PropertyEditing.Selection { using System; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Runtime; using System.Activities.Presentation.Internal.PropertyEditing.Selection; using System.Activities.Presentation; // <summary> // This is a container for attached properties used by PropertyInspector to track and manage // property selection. It is public because WPF requires that attached properties used in XAML // be declared by public classes. // </summary> [EditorBrowsable(EditorBrowsableState.Never)] static class PropertySelection { private static readonly DependencyPropertyKey IsSelectedPropertyKey = DependencyProperty.RegisterAttachedReadOnly( "IsSelected", typeof(bool), typeof(PropertySelection), new PropertyMetadata(false)); // <summary> // Attached, ReadOnly DP that we use to mark objects as selected. If they care, they can then render // themselves differently. // </summary> internal static readonly DependencyProperty IsSelectedProperty = IsSelectedPropertyKey.DependencyProperty; // <summary> // Attached DP that we use in XAML to mark elements that can be selected. // </summary> internal static readonly DependencyProperty SelectionStopProperty = DependencyProperty.RegisterAttached( "SelectionStop", typeof(ISelectionStop), typeof(PropertySelection), new PropertyMetadata(null)); // <summary> // Attached DP used in conjunction with SelectionStop DP. It specifies the FrameworkElement to hook into // in order to handle double-click events to control the expanded / collapsed state of its parent SelectionStop. // </summary> internal static readonly DependencyProperty IsSelectionStopDoubleClickTargetProperty = DependencyProperty.RegisterAttached( "IsSelectionStopDoubleClickTarget", typeof(bool), typeof(PropertySelection), new PropertyMetadata(false, new PropertyChangedCallback(OnIsSelectionStopDoubleClickTargetChanged))); // <summary> // Attached DP that we use in XAML to mark elements as selection scopes - meaning selection // won't spill beyond the scope of the marked element. // </summary> internal static readonly DependencyProperty IsSelectionScopeProperty = DependencyProperty.RegisterAttached( "IsSelectionScope", typeof(bool), typeof(PropertySelection), new PropertyMetadata(false)); // <summary> // Attached property we use to route non-navigational key strokes from one FrameworkElement to // another. When this property is set on a FrameworkElement, we hook into its KeyDown event // and send any unhandled, non-navigational key strokes to the FrameworkElement specified // by this property. The target FrameworkElement must be focusable or have a focusable child. // When the first eligible key stroke is detected, the focus will be shifted to the focusable // element and the key stroke will be sent to it. // </summary> internal static readonly DependencyProperty KeyDownTargetProperty = DependencyProperty.RegisterAttached( "KeyDownTarget", typeof(FrameworkElement), typeof(PropertySelection), new PropertyMetadata(null, new PropertyChangedCallback(OnKeyDownTargetChanged))); // Constant that determines how deep in the visual tree we search for SelectionStops that // are children or neighbors of a given element (usually one that the user clicked on) before // giving up. This constant is UI-dependent. private const int MaxSearchDepth = 11; // <summary> // Gets PropertySelection.IsSelected property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to examine</param> // <returns>Value of the IsSelected property</returns> internal static bool GetIsSelected(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } return (bool)obj.GetValue(IsSelectedProperty); } // Private (internal) setter that we use to mark objects as selected from within CategoryList class // internal static void SetIsSelected(DependencyObject obj, bool value) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.SetValue(IsSelectedPropertyKey, value); } // SelectionStop Attached DP // <summary> // Gets PropertySelection.SelectionStop property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to examine</param> // <returns>Value of the SelectionStop property.</returns> internal static ISelectionStop GetSelectionStop(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } return (ISelectionStop)obj.GetValue(SelectionStopProperty); } // <summary> // Sets PropertySelection.SelectionStop property on the specified DependencyObject // </summary> // <param name="obj">DependencyObject to modify</param> // <param name="value">New value of SelectionStop</param> internal static void SetSelectionStop(DependencyObject obj, ISelectionStop value) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.SetValue(SelectionStopProperty, value); } // <summary> // Clears PropertySelection.SelectionStop property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to clear</param> internal static void ClearSelectionStop(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.ClearValue(SelectionStopProperty); } // IsSelectionStopDoubleClickTarget Attached DP // <summary> // Gets PropertySelection.IsSelectionStopDoubleClickTarget property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to examine</param> // <returns>Value of the IsSelectionStopDoubleClickTarget property.</returns> internal static bool GetIsSelectionStopDoubleClickTarget(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } return (bool)obj.GetValue(IsSelectionStopDoubleClickTargetProperty); } // <summary> // Sets PropertySelection.IsSelectionStopDoubleClickTarget property on the specified DependencyObject // </summary> // <param name="obj">DependencyObject to modify</param> // <param name="value">New value of IsSelectionStopDoubleClickTarget</param> internal static void SetIsSelectionStopDoubleClickTarget(DependencyObject obj, bool value) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.SetValue(IsSelectionStopDoubleClickTargetProperty, value); } // <summary> // Clears PropertySelection.IsSelectionStopDoubleClickTarget property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to modify</param> internal static void ClearIsSelectionStopDoubleClickTarget(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.ClearValue(IsSelectionStopDoubleClickTargetProperty); } // Called when some object gets specified as the SelectionStop double-click target: // // * Hook into the MouseDown event so that we can detect double-clicks and automatically // expand or collapse the corresponding SelectionStop, if possible // private static void OnIsSelectionStopDoubleClickTargetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { FrameworkElement target = sender as FrameworkElement; if (target == null) { return; } if (bool.Equals(e.OldValue, false) && bool.Equals(e.NewValue, true)) { AddDoubleClickHandler(target); } else if (bool.Equals(e.OldValue, true) && bool.Equals(e.NewValue, false)) { RemoveDoubleClickHandler(target); } } // Called when some SelectionStop double-click target gets unloaded: // // * Unhook from events so that we don't prevent garbage collection // private static void OnSelectionStopDoubleClickTargetUnloaded(object sender, RoutedEventArgs e) { FrameworkElement target = sender as FrameworkElement; Fx.Assert(target != null, "sender parameter should not be null"); if (target == null) { return; } RemoveDoubleClickHandler(target); } // Called when the UI object representing a SelectionStop gets clicked: // // * If this is a double-click and the SelectionStop can be expanded / collapsed, // expand / collapse the SelectionStop // private static void OnSelectionStopDoubleClickTargetMouseDown(object sender, MouseButtonEventArgs e) { DependencyObject target = e.OriginalSource as DependencyObject; if (target == null) { return; } if (e.ClickCount > 1) { FrameworkElement parentSelectionStopVisual = PropertySelection.FindParentSelectionStop<FrameworkElement>(target); if (parentSelectionStopVisual != null) { ISelectionStop parentSelectionStop = PropertySelection.GetSelectionStop(parentSelectionStopVisual); if (parentSelectionStop != null && parentSelectionStop.IsExpandable) { parentSelectionStop.IsExpanded = !parentSelectionStop.IsExpanded; } } } } private static void AddDoubleClickHandler(FrameworkElement target) { target.AddHandler(UIElement.MouseDownEvent, new MouseButtonEventHandler(OnSelectionStopDoubleClickTargetMouseDown), false); target.Unloaded += new RoutedEventHandler(OnSelectionStopDoubleClickTargetUnloaded); } private static void RemoveDoubleClickHandler(FrameworkElement target) { target.Unloaded -= new RoutedEventHandler(OnSelectionStopDoubleClickTargetUnloaded); target.RemoveHandler(UIElement.MouseDownEvent, new MouseButtonEventHandler(OnSelectionStopDoubleClickTargetMouseDown)); } // IsSelectionScope Attached DP // <summary> // Gets PropertySelection.IsSelectionScope property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to examine</param> // <returns>Value of the IsSelectionScope property.</returns> internal static bool GetIsSelectionScope(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } return (bool)obj.GetValue(IsSelectionScopeProperty); } // <summary> // Sets PropertySelection.IsSelectionScope property on the specified DependencyObject // </summary> // <param name="obj">DependencyObject to modify</param> // <param name="value">New value of IsSelectionScope</param> internal static void SetIsSelectionScope(DependencyObject obj, bool value) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.SetValue(IsSelectionScopeProperty, value); } // KeyDownTarget Attached DP // <summary> // Gets PropertySelection.KeyDownTarget property from the specified DependencyObject // </summary> // <param name="obj">DependencyObject to examine</param> // <returns>Value of the KeyDownTarget property.</returns> internal static FrameworkElement GetKeyDownTarget(DependencyObject obj) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } return (FrameworkElement)obj.GetValue(KeyDownTargetProperty); } // <summary> // Sets PropertySelection.KeyDownTarget property on the specified DependencyObject // </summary> // <param name="obj">DependencyObject to modify</param> // <param name="value">New value of KeyDownTarget</param> internal static void SetKeyDownTarget(DependencyObject obj, FrameworkElement value) { if (obj == null) { throw FxTrace.Exception.ArgumentNull("obj"); } obj.SetValue(KeyDownTargetProperty, value); } // Called when some FrameworkElement gets specified as the target for KeyDown RoutedEvents - // hook into / unhook from the KeyDown event of the source private static void OnKeyDownTargetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { FrameworkElement target = sender as FrameworkElement; if (target == null) { return; } if (e.OldValue != null && e.NewValue == null) { RemoveKeyStrokeHandlers(target); } else if (e.NewValue != null && e.OldValue == null) { AddKeyStrokeHandlers(target); } } // Called when a KeyDownTarget gets unloaded - // unhook from events so that we don't prevent garbage collection private static void OnKeyDownTargetUnloaded(object sender, RoutedEventArgs e) { FrameworkElement target = sender as FrameworkElement; Fx.Assert(target != null, "sender parameter should not be null"); if (target == null) { return; } RemoveKeyStrokeHandlers(target); } // Called when a KeyDownTarget is specified and a KeyDown event is detected on the source private static void OnKeyDownTargetKeyDown(object sender, KeyEventArgs e) { // Ignore handled events if (e.Handled) { return; } // Ignore navigation keys if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up || e.Key == Key.Down || e.Key == Key.Tab || e.Key == Key.Escape || e.Key == Key.Return || e.Key == Key.Enter || e.Key == Key.PageUp || e.Key == Key.PageDown || e.Key == Key.Home || e.Key == Key.End || e.Key == Key.LeftCtrl || e.Key == Key.RightCtrl) { return; } if (Keyboard.Modifiers == ModifierKeys.Control) { return; } DependencyObject keySender = sender as DependencyObject; Fx.Assert(keySender != null, "keySender should not be null"); if (keySender == null) { return; } FrameworkElement keyTarget = GetKeyDownTarget(keySender); Fx.Assert(keyTarget != null, "keyTarget should not be null"); if (keyTarget == null) { return; } // Find a focusable element on the target, set focus to it, and send the keys over FrameworkElement focusable = VisualTreeUtils.FindFocusableElement<FrameworkElement>(keyTarget); if (focusable != null && focusable == Keyboard.Focus(focusable)) { focusable.RaiseEvent(e); } } private static void AddKeyStrokeHandlers(FrameworkElement target) { target.AddHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnKeyDownTargetKeyDown), false); target.Unloaded += new RoutedEventHandler(OnKeyDownTargetUnloaded); } private static void RemoveKeyStrokeHandlers(FrameworkElement target) { target.Unloaded -= new RoutedEventHandler(OnKeyDownTargetUnloaded); target.RemoveHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnKeyDownTargetKeyDown)); } // <summary> // Returns the closest parent (or the element itself) marked as a SelectionStop. // </summary> // <typeparam name="T">Type of element to look for</typeparam> // <param name="element">Element to examine</param> // <returns>The closest parent (or the element itself) marked as a SelectionStop; // null if not found.</returns> internal static T FindParentSelectionStop<T>(DependencyObject element) where T : DependencyObject { if (element == null) { return null; } do { // IsEligibleSelectionStop already checks for visibility, so we don't need to // to do a specific check somewhere else in this loop if (IsEligibleSelectionStop<T>(element)) { return (T)element; } element = VisualTreeHelper.GetParent(element); } while (element != null); return null; } // <summary> // Returns the closest neighbor in the given direction marked as a SelectionStop. // </summary> // <typeparam name="T">Type of element to look for</typeparam> // <param name="element">Element to examine</param> // <param name="direction">Direction to search in</param> // <returns>The closest neighboring element in the given direction marked as a IsSelectionStop, // if found, null otherwise.</returns> internal static T FindNeighborSelectionStop<T>(DependencyObject element, SearchDirection direction) where T : DependencyObject { if (element == null) { throw FxTrace.Exception.ArgumentNull("element"); } T neighbor; int maxSearchDepth = MaxSearchDepth; // If we are looking for the NEXT element and we can dig deeper, start by digging deeper // before trying to look for any siblings. // if (direction == SearchDirection.Next && IsExpanded(element)) { neighbor = FindChildSelectionStop<T>(element, 0, VisualTreeHelper.GetChildrenCount(element) - 1, direction, maxSearchDepth, MatchDirection.Down); if (neighbor != null) { return neighbor; } } int childIndex, childrenCount, childDepth; bool isParentSelectionStop, isParentSelectionScope = false; DependencyObject parent = element; while (true) { while (true) { // If we reached the selection scope, don't try to go beyond it if (isParentSelectionScope) { return null; } parent = GetEligibleParent(parent, out childIndex, out childrenCount, out childDepth, out isParentSelectionStop, out isParentSelectionScope); maxSearchDepth += childDepth; if (parent == null) { return null; } if (direction == SearchDirection.Next && (childIndex + 1) >= childrenCount) { continue; } if (direction == SearchDirection.Previous && isParentSelectionStop == false && (childIndex < 1)) { continue; } break; } // If we get here, that means we found a SelectionStop on which we need to look for children that are // SelectionStops themselves. The first such child found should be returned. Otherwise, if no such child // is found, we potentially look at the node itself and return it OR we repeat the process and keep looking // for a better parent. int leftIndex, rightIndex; MatchDirection matchDirection; if (direction == SearchDirection.Previous) { leftIndex = 0; rightIndex = childIndex - 1; matchDirection = MatchDirection.Up; } else { leftIndex = childIndex + 1; rightIndex = childrenCount - 1; matchDirection = MatchDirection.Down; } neighbor = FindChildSelectionStop<T>(parent, leftIndex, rightIndex, direction, maxSearchDepth, matchDirection); if (neighbor != null) { return neighbor; } if (direction == SearchDirection.Previous && IsEligibleSelectionStop<T>(parent)) { return (T)parent; } } } // Helper method used from GetNeighborSelectionStop() // Returns a parent DependencyObject of the specified element that is // // * Visible AND // * ( Marked with a SelectionStop OR // * Marked with IsSelectionScope = true OR // * Has more than one child ) // private static DependencyObject GetEligibleParent(DependencyObject element, out int childIndex, out int childrenCount, out int childDepth, out bool isSelectionStop, out bool isSelectionScope) { childDepth = 0; isSelectionStop = false; isSelectionScope = false; bool isVisible; do { element = VisualTreeUtils.GetIndexedVisualParent(element, out childrenCount, out childIndex); isSelectionStop = element == null ? false : (GetSelectionStop(element) != null); isSelectionScope = element == null ? false : GetIsSelectionScope(element); isVisible = VisualTreeUtils.IsVisible(element as UIElement); childDepth++; } while ( element != null && (isVisible == false || (isSelectionStop == false && isSelectionScope == false && childrenCount < 2))); return element; } // Helper method that performs a recursive, depth-first search of children starting at the specified parent, // looking for any children that conform to the specified Type and are marked with a SelectionStop // private static T FindChildSelectionStop<T>(DependencyObject parent, int leftIndex, int rightIndex, SearchDirection iterationDirection, int maxDepth, MatchDirection matchDirection) where T : DependencyObject { if (parent == null || maxDepth <= 0) { return null; } int step = iterationDirection == SearchDirection.Next ? 1 : -1; int index = iterationDirection == SearchDirection.Next ? leftIndex : rightIndex; for (; index >= leftIndex && index <= rightIndex; index = index + step) { DependencyObject child = VisualTreeHelper.GetChild(parent, index); // If MatchDirection is set to Down, do an eligibility match BEFORE we dive down into // more children. // if (matchDirection == MatchDirection.Down && IsEligibleSelectionStop<T>(child)) { return (T)child; } // If this child is not an eligible SelectionStop because it is not visible, // there is no point digging down to get to more children. // if (!VisualTreeUtils.IsVisible(child as UIElement)) { continue; } int grandChildrenCount = VisualTreeHelper.GetChildrenCount(child); if (grandChildrenCount > 0 && IsExpanded(child)) { T element = FindChildSelectionStop<T>(child, 0, grandChildrenCount - 1, iterationDirection, maxDepth - 1, matchDirection); if (element != null) { return element; } } // If MatchDirection is set to Up, do an eligibility match AFTER we tried diving into // more children and failed to find something we could return. // if (matchDirection == MatchDirection.Up && IsEligibleSelectionStop<T>(child)) { return (T)child; } } return null; } // Helper method that returns false if the given element is a collapsed SelectionStop, // true otherwise. // private static bool IsExpanded(DependencyObject element) { ISelectionStop selectionStop = PropertySelection.GetSelectionStop(element); return selectionStop == null || selectionStop.IsExpanded; } // Helper method that return true if the given element is marked with a SelectionStop, // if it derives from the specified Type, and if it is Visible (assuming it derives from UIElement) // private static bool IsEligibleSelectionStop<T>(DependencyObject element) where T : DependencyObject { return (GetSelectionStop(element) != null) && typeof(T).IsAssignableFrom(element.GetType()) && VisualTreeUtils.IsVisible(element as UIElement); } // <summary> // Private enum we use to specify whether FindSelectionStopChild() should return any matches // as it drills down into the visual tree (Down) or whether it should wait on looking at // matches until it's bubbling back up again (Up). // </summary> private enum MatchDirection { Down, Up } // IsSelected ReadOnly, Attached DP } } |