[reflection] Coop handles icalls in System.Reflection and System.RuntimeTypeHandle...
[mono.git] / mcs / class / referencesource / System.Activities.Presentation / System.Activities.Presentation / System / Activities / Presentation / Base / Core / Internal / PropertyEditing / CategoryList.cs
1 //----------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation.  All rights reserved.
3 //----------------------------------------------------------------
4 namespace System.Activities.Presentation.Internal.PropertyEditing 
5 {
6     using System;
7     using System.Collections.Generic;
8     using System.ComponentModel;
9     using System.Diagnostics;
10     using System.Diagnostics.CodeAnalysis;
11     using System.Globalization;
12     using System.Windows;
13     using System.Windows.Automation.Peers;
14     using System.Windows.Controls;
15     using System.Windows.Input;
16     using System.Windows.Threading;
17
18     using System.Activities.Presentation;
19     using System.Activities.Presentation.PropertyEditing;
20
21     using System.Activities.Presentation.Internal.PropertyEditing.Automation;
22     using System.Activities.Presentation.Internal.PropertyEditing.FromExpression.Framework;
23     using System.Activities.Presentation.Internal.PropertyEditing.FromExpression.Framework.Data;
24     using System.Activities.Presentation.Internal.PropertyEditing.FromExpression.Framework.PropertyInspector;
25     using System.Activities.Presentation.Internal.PropertyEditing.Model;
26     using System.Activities.Presentation.Internal.PropertyEditing.Selection;
27     using System.Activities.Presentation.Internal.PropertyEditing.State;
28
29     // <summary>
30     // Wrapper around ItemsControl that knows how to contain and display CategoryEntries,
31     // deals with AutomationPeers, and persists the open and closed state of individual
32     // CategoryContainers.
33     //
34     // This class should ideally be internal, but Avalon can't handle attached properties
35     // (which this class defines) on internal classes.
36     // </summary>
37     internal class CategoryList : ItemsControl, IEnumerable<CategoryBase>, IStateContainer, INotifyPropertyChanged 
38     {
39
40         // This guy is static so that its values persist across designers and CategoryList instances
41         private static CategoryStateContainer _categoryStates = new CategoryStateContainer();
42
43         // This guy is not because it caches FilterString, which is specific to each CategoryList instance
44         private IStateContainer _stateContainer;
45
46         // Used for property selection
47         private FrameworkElement _selection;
48         private PropertySelectionMode _selectionMode;
49
50         // Used for property filtering
51         private string _filterString;
52         private PropertyFilter _currentFilter;
53         private bool _hasAnyFilterMatches = true;
54         private ICommand _clearFilterCommand;
55
56         // Miscelaneous
57         private SharedPropertyValueColumnWidthContainer _sharedWidthContainer;
58
59         [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
60         static CategoryList() 
61         {
62             // Make CategoryList non-focusable by default
63             UIElement.FocusableProperty.OverrideMetadata(typeof(CategoryList), new FrameworkPropertyMetadata(false));
64
65             // Mark the uber-CategoryList as the scope for property selection
66             PropertySelection.IsSelectionScopeProperty.OverrideMetadata(typeof(CategoryList), new PropertyMetadata(true));
67         }
68
69         // <summary>
70         // Basic ctor
71         // </summary>
72         [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
73         public CategoryList() 
74         {
75
76             _stateContainer = new AggregateStateContainer(
77                 new CategoryListStateContainer(this),
78                 _categoryStates);
79
80             // Setup the shared width container
81             _sharedWidthContainer = new SharedPropertyValueColumnWidthContainer();
82             SharedPropertyValueColumnWidthContainer.SetOwningSharedPropertyValueColumnWidthContainer(this, _sharedWidthContainer);
83
84             // When someone new gets focus, we may need to mess around with selected property, so listen to the event
85             this.AddHandler(FocusManager.GotFocusEvent, new RoutedEventHandler(OnSomeoneGotFocus));
86
87             // When editing is done in the value editor, shift focus back to the selected property
88             this.CommandBindings.Add(new CommandBinding(PropertyValueEditorCommands.FinishEditing, OnFinishEditing));
89
90             // Need to call this method from a UI thread because some of Sparkle's value editors rely on it
91             UIThreadDispatcher.InitializeInstance();
92         }
93
94         // <summary>
95         // Event fired whenever a new CategoryContainer instance is generated
96         // </summary>
97         public event ContainerGeneratedHandler ContainerGenerated;
98
99         // AutomationPeer
100
101         public event PropertyChangedEventHandler PropertyChanged;
102
103         public int Count 
104         {
105             get {
106                 return this.Items.Count;
107             }
108         }
109
110         // <summary>
111         // Gets or sets the filter string to apply to the shown properties.
112         // Setting this value will automatically apply the filter to the shown properties.
113         // </summary>
114         public string FilterString 
115         {
116             get {
117                 return _filterString;
118             }
119             set {
120                 if (string.IsNullOrEmpty(value))
121                 {
122                     value = null;
123                 }
124
125                 if (_filterString != value) 
126                 {
127                     _filterString = value;
128                     _currentFilter = new PropertyFilter(_filterString);
129                     RefreshFilter();
130                     this.OnPropertyChanged("FilterString");
131                 }
132             }
133         }
134
135         // <summary>
136         // Command-wrapper for the ClearFilter method
137         // </summary>
138         public ICommand ClearFilterCommand 
139         {
140             get {
141                 if (_clearFilterCommand == null) 
142                 {
143                     _clearFilterCommand = new DelegateCommand(this.ClearFilter);
144                 }
145                 return _clearFilterCommand;
146             }
147         }
148
149         // <summary>
150         // Gets a value indicating whether there are any categories or properties that
151         // match the current filter string
152         // </summary>
153         public bool HasAnyFilterMatches 
154         {
155             get {
156                 return _hasAnyFilterMatches;
157             }
158             private set {
159                 if (_hasAnyFilterMatches != value) 
160                 {
161                     _hasAnyFilterMatches = value;
162                     this.OnPropertyChanged("HasAnyFilterMatches");
163                 }
164             }
165         }
166
167         // <summary>
168         // Gets the currently selected visual.
169         // </summary>
170         public FrameworkElement Selection 
171         {
172             get {
173                 return _selection;
174             }
175         }
176
177         // <summary>
178         // Gets or sets the path to the currently selected item in the CategoryList.
179         // Only "sticky" selections will return a valid SelectionPath - otherwise
180         // null is returned.  Setting this property changes the selection if the specified path
181         // can be resolved to a FrameworkElement to select and sets the SelectionMode to
182         // Sticky.
183         // </summary>
184         public SelectionPath SelectionPath 
185         {
186             get {
187                 if (_selection == null) 
188                 {
189                     return null;
190                 }
191                 if (this.SelectionMode != PropertySelectionMode.Sticky) 
192                 {
193                     return null;
194                 }
195
196                 ISelectionStop selectionStop = PropertySelection.GetSelectionStop(_selection);
197                 if (selectionStop == null) 
198                 {
199                     return null;
200                 }
201
202                 return selectionStop.Path;
203             }
204             set {
205                 SetSelectionPath(value);
206             }
207         }
208
209         // <summary>
210         // Gets or sets the current SelectionMode
211         // </summary>
212         private PropertySelectionMode SelectionMode 
213         {
214             get { return _selectionMode; }
215             set { _selectionMode = value; }
216         }
217
218         public CategoryEntry this[int index] {
219             get {
220                 return (CategoryEntry)this.Items[index];
221             }
222         }
223
224         protected override AutomationPeer OnCreateAutomationPeer() 
225         {
226             return new CategoryListAutomationPeer(this);
227         }
228
229
230         // Convenience Accessors
231
232         public void Insert(int index, CategoryEntry category) 
233         {
234             if (category == null)
235             {
236                 throw FxTrace.Exception.ArgumentNull("category");
237             }
238
239             this.Items.Insert(index, category);
240         }
241
242         public void InsertAlphabetically(CategoryEntry category) 
243         {
244
245             // POSSIBLE OPTIMIZATION: optimize using the fact that the list of categories in this
246             // collection is ordered.
247             int index = 0;
248             for (; index < this.Count; index++) 
249             {
250                 if (string.Compare(category.CategoryName, this[index].CategoryName, StringComparison.CurrentCulture) < 0)
251                 {
252                     break;
253                 }
254             }
255
256             this.Insert(index, category);
257         }
258
259         public void RemoveAt(int index) 
260         {
261             this.Items.RemoveAt(index);
262         }
263
264
265         // Command Handlers
266
267         private void OnFinishEditing(object sender, ExecutedRoutedEventArgs e) 
268         {
269             // Re-focus the selected selection stop
270             this.SynchronizeSelectionFocus(StealFocusMode.Always);
271         }
272
273
274         // IStateContainer
275
276         public void RestoreState(object state) 
277         {
278             _stateContainer.RestoreState(state);
279         }
280
281         public object RetrieveState() 
282         {
283             return _stateContainer.RetrieveState();
284         }
285
286         // This override both gets rid of the inbetween ContentPresenter and
287         // it makes the CategoryContainer available as an instance in PrepareContainerForItemOverride()
288         protected override DependencyObject GetContainerForItemOverride() 
289         {
290             return new CiderCategoryContainer();
291         }
292
293         // Set the expansion state on the CategoryContainer based on an existing container
294         // or a cached value for the contained category
295         protected override void PrepareContainerForItemOverride(DependencyObject element, object item) 
296         {
297             CiderCategoryContainer container = element as CiderCategoryContainer;
298             CategoryEntry category = item as CategoryEntry;
299
300             if (container != null && category != null) 
301             {
302
303                 // Ideally, we would want to initalize the expansion state here.  However,
304                 // in collection editor, Blend messes around with the expansion state _after_
305                 // the call to this method, which breaks our ability to remember which categories
306                 // were open and which were closed.  So, we set the state here (because Blend's
307                 // search logic relies on it to be set by this point) and _reset_ it in the Loaded
308                 // event handler.
309
310                 // Look into stored state
311                 CategoryState state = _categoryStates.GetCategoryState(category.CategoryName);
312                 container.Expanded = state.CategoryExpanded;
313                 container.AdvancedSectionPinned = state.AdvancedSectionExpanded;
314
315                 // Hook into other event handlers that we care about (including the Loaded event)
316                 container.Loaded += new RoutedEventHandler(OnContainerLoaded);
317                 container.Unloaded += new RoutedEventHandler(OnContainerUnloaded);
318
319                 if (ContainerGenerated != null)
320                 {
321                     ContainerGenerated(this, new ContainerGeneratedEventArgs(container));
322                 }
323             }
324             else 
325             {
326                 Debug.Fail("CategoryList should only be populated with CategoryEntries.");
327             }
328
329             base.PrepareContainerForItemOverride(element, item);
330         }
331
332         // Re-initialize the expansion state here and hook into the ExpandedChanged and
333         // AdvancedSectionPinnedChanged events, because by now Blend may have ----ed those
334         // two values up (see comment in PrepareContainerForItemOverride() )
335         private void OnContainerLoaded(object sender, RoutedEventArgs e) 
336         {
337             CiderCategoryContainer container = sender as CiderCategoryContainer;
338
339             if (container != null) 
340             {
341
342                 CategoryEntry category = container.Category;
343                 if (category != null) 
344                 {
345                     // Look into stored state
346                     CategoryState state = _categoryStates.GetCategoryState(category.CategoryName);
347                     container.Expanded = state.CategoryExpanded;
348                     container.AdvancedSectionPinned = state.AdvancedSectionExpanded;
349
350                     // Hook into these events here, because Blend won't mess the state up at this point
351                     container.ExpandedChanged += new EventHandler(OnContainerExpandedChanged);
352                     container.AdvancedSectionPinnedChanged += new EventHandler(OnAdvancedSectionPinnedChanged);
353                 }
354             }
355             else 
356             {
357                 Debug.Fail("CategoryList expects the individual items to be CiderCategoryContainers");
358             }
359         }
360
361         private void OnContainerUnloaded(object sender, RoutedEventArgs e) 
362         {
363             CiderCategoryContainer container = sender as CiderCategoryContainer;
364             if (container != null) 
365             {
366                 // Unhook from any events that we used to care about
367                 container.ExpandedChanged -= new EventHandler(OnContainerExpandedChanged);
368                 container.AdvancedSectionPinnedChanged -= new EventHandler(OnAdvancedSectionPinnedChanged);
369                 container.Loaded -= new RoutedEventHandler(OnContainerLoaded);
370                 container.Unloaded -= new RoutedEventHandler(OnContainerUnloaded);
371             }
372             else 
373             {
374                 Debug.Fail("Couldn't clean up event binding and store container state.");
375             }
376         }
377
378         private void OnContainerExpandedChanged(object sender, EventArgs e) 
379         {
380             // If we are in "Filter-applied" mode, don't store the expansion state, since applying
381             // the filter automatically expands everything that matches that filter
382             if (_currentFilter != null && !_currentFilter.IsEmpty)
383             {
384                 return;
385             }
386
387             CiderCategoryContainer container = sender as CiderCategoryContainer;
388             if (container != null) 
389             {
390                 CategoryEntry category = container.Category;
391                 if (category != null) 
392                 {
393                     CategoryState state = _categoryStates.GetCategoryState(container.Category.CategoryName);
394                     state.CategoryExpanded = container.Expanded;
395                 }
396             }
397         }
398
399         private void OnAdvancedSectionPinnedChanged(object sender, EventArgs e) 
400         {
401             // If we are in "Filter-applied" mode, don't store the expansion state, since applying
402             // the filter automatically expands everything that matches that filter
403             if (_currentFilter != null && !_currentFilter.IsEmpty)
404             {
405                 return;
406             }
407
408             CiderCategoryContainer container = sender as CiderCategoryContainer;
409             if (container != null) 
410             {
411                 CategoryEntry category = container.Category;
412                 if (category != null) 
413                 {
414                     CategoryState state = _categoryStates.GetCategoryState(container.Category.CategoryName);
415                     state.AdvancedSectionExpanded = container.AdvancedSectionPinned;
416                 }
417             }
418         }
419
420         // Searching
421
422         // <summary>
423         // Applies the current filter to the existing list of categories and properties
424         // and updates the value of HasAnyFilterMatches.  This class does not update itself
425         // automatically when new CategoryEntries are added or removed, so call this method
426         // explicitly when things change.
427         // </summary>
428         public void RefreshFilter() 
429         {
430             bool? matchesFilter = null;
431
432             foreach (CategoryBase category in this.Items) 
433             {
434                 matchesFilter = matchesFilter == null ? false : matchesFilter;
435                 matchesFilter |= ApplyFilter(_currentFilter, category);
436             }
437
438             this.HasAnyFilterMatches = matchesFilter == null ? true : (bool)matchesFilter;
439         }
440
441         // <summary>
442         // Clears the current property filter, if any
443         // </summary>
444         public void ClearFilter() 
445         {
446             this.FilterString = null;
447         }
448
449         // Applies the specified filter to the specified category, returning a boolean indicating
450         // whether anything in that category matched the filter or not
451         private static bool ApplyFilter(PropertyFilter filter, CategoryBase category) 
452         {
453             category.ApplyFilter(filter);
454             return category.MatchesFilter || category.BasicPropertyMatchesFilter || category.AdvancedPropertyMatchesFilter;
455         }
456
457
458         // Property Selection
459
460         // <summary>
461         // Sets the SelectionMode back to default.  This is a common enough
462         // operation that it makes sense to abstract it to its own method.
463         // </summary>
464         private void ResetSelectionMode() 
465         {
466             SelectionMode = PropertySelectionMode.Default;
467         }
468
469         // <summary>
470         // Updates property selection to the specified SelectionPath (if any)
471         // or the specified default property.  Returns true if some property was selected,
472         // false otherwise (such as in the case when no properties are showing).
473         // </summary>
474         // <param name="stickyPath">SelectionPath to select.  Takes precedence over default property.</param>
475         // <param name="defaultPropertyName">Property to select when no SelectionPath is specified or
476         // if the path cannot be resolved.</param>
477         // <param name="fallbackPath">SelectionPath to use when all else fails.  May be null.</param>
478         // <returns>True if some property was selected, false otherwise (such as in the case
479         // when no properties are showing).</returns>
480         public bool UpdateSelectedProperty(
481             SelectionPath stickyPath,
482             string defaultPropertyName,
483             SelectionPath fallbackPath) 
484         {
485
486             // First, try selecting the given stickyPath, if any
487             //
488             if (stickyPath == null || !SetSelectionPath(stickyPath))
489             {
490                 ResetSelectionMode();
491             }
492
493             bool propertySelected;
494
495             if (SelectionMode == PropertySelectionMode.Default) 
496             {
497
498                 // Then, try finding and selecting the default property
499                 //
500                 propertySelected = defaultPropertyName == null ? false : SelectDefaultProperty(defaultPropertyName);
501                 if (!propertySelected && fallbackPath != null) 
502                 {
503
504                     // And if that fails, go to the specified fallback SelectionPath,
505                     // if any
506                     //
507                     propertySelected = SetSelectionPath(fallbackPath);
508                 }
509
510                 // Make sure that we are still in Default selection mode
511                 // at this point
512                 //
513                 ResetSelectionMode();
514             }
515             else 
516             {
517                 propertySelected = true;
518             }
519
520             return propertySelected;
521         }
522
523         private bool SetSelectionPath(SelectionPath path) 
524         {
525             // Dummy, this variable is only to satisfy SetSelectionPath(SelectionPath path, out bool pendingGeneration)
526             bool isPendingGenerationDummy;
527             return SetSelectionPath(path, out isPendingGenerationDummy);
528         }
529
530         // Attempts to resolve the specified path and set it as the current selection, returning
531         // true or false based on success.  If the path is found, selection is set to Sticky.
532         // If the UI is not ready, return false and pendingGeneration is true.
533         internal bool SetSelectionPath(SelectionPath path, out bool pendingGeneration)
534         {
535             DependencyObject newSelection = SelectionPathResolver.ResolveSelectionPath(this, path, out pendingGeneration);
536             if (newSelection != null)
537             {
538                 SelectAndFocus(newSelection, StealFocusMode.OnlyIfCategoryListHasFocusWithin);
539                 this.SelectionMode = PropertySelectionMode.Sticky;
540                 return true;
541             }
542
543             return false;
544         }
545
546         // When the user clicks somewhere, we try to find the closest parent with IsSelectionStop DP set to true and
547         // select it.
548         //
549         protected override void OnPreviewMouseDown(MouseButtonEventArgs e) 
550         {
551             Select(e.OriginalSource as DependencyObject);
552             base.OnPreviewMouseDown(e);
553         }
554
555         // If we set Focus in OnMouseDown, it would be overwritten by the time the mouse went up.  So, we set the focus
556         // in OnMouseUp() to make sure it sticks
557         //
558         protected override void OnPreviewMouseUp(MouseButtonEventArgs e) 
559         {
560             base.OnPreviewMouseUp(e);
561             SynchronizeSelectionFocus(StealFocusMode.OnlyIfCurrentSelectionDoesNotHaveFocusWithin);
562         }
563
564         // When a UIElement gets focus, we try to find the parent PropertyContainer and make sure
565         // it's selected
566         //
567         private void OnSomeoneGotFocus(object source, RoutedEventArgs e) 
568         {
569             Select(e.OriginalSource as DependencyObject);
570         }
571
572         // We only synchronize the IsSelected object with keyboard focus when the CategoryList itself
573         // has focus.  If it doesn't we don't want to accidentally steal focus from somewhere else
574         // (say the design surface when the user is just tabbing through objects on it).  However, once
575         // the CategoryList itself gains focus, we do want to synchronize the IsSelected object with 
576         // keyboard focus so that Tabs and keyboard navigation works correctly.
577         //
578         protected override void OnIsKeyboardFocusWithinChanged(DependencyPropertyChangedEventArgs e) 
579         {
580
581             bool hasKeyboardFocus = (bool)e.NewValue;
582
583             if (hasKeyboardFocus) 
584             {
585                 // We just gained keyboard focus.  Make sure we [....] up the current property selection
586                 // with the keyboard focus, so that navigation works.
587                 SynchronizeSelectionFocus(StealFocusMode.OnlyIfCurrentSelectionDoesNotHaveFocusWithin);
588             }
589
590             base.OnIsKeyboardFocusWithinChanged(e);
591         }
592
593         // Helper method that ensures that the element marked as IsSelected also has focus.
594         // If it cannot receive focus, we look for its closest visual child that can
595         //
596         private void SynchronizeSelectionFocus(StealFocusMode focusMode) 
597         {
598             // Is there something to select?
599             if (_selection == null)
600             {
601                 return;
602             }
603
604             // Are we even allowed to mess around with focus or does someone else on the
605             // design surface have focus right now and we should just let them have it?
606             if (focusMode == StealFocusMode.OnlyIfCategoryListHasFocusWithin && !this.IsKeyboardFocusWithin)
607             {
608                 return;
609             }
610
611             if (focusMode == StealFocusMode.OnlyIfCurrentSelectionDoesNotHaveFocusWithin && _selection.IsKeyboardFocusWithin)
612             {
613                 return;
614             }
615
616             FrameworkElement focusableElement = VisualTreeUtils.FindFocusableElement<FrameworkElement>(_selection);
617
618             if (focusableElement != null) 
619             {
620                 focusableElement.Focus();
621             }
622         }
623
624         // Attempt to select the right thing for the specified default property name:
625         //
626         //  * If there is a common DefaultProperty among selected objects AND there is a CategoryEditor consuming it,
627         //    select the CategoryEditor
628         //
629         //  * If there is a common DefaultProperty among selected objects AND there is NO CategoryEditor consuming it,
630         //    select the property itself
631         //
632         //  * If there is no common DefaultProperty, select the first common category
633         //
634         //  * Otherwise fail by returning false
635         //
636         private bool SelectDefaultProperty(string defaultPropertyName) 
637         {
638             return SelectDefaultPropertyHelper(defaultPropertyName, true);
639         }
640
641         private bool SelectDefaultPropertyHelper(string defaultPropertyName, bool firstTime) 
642         {
643             if (string.IsNullOrEmpty(defaultPropertyName))
644             {
645                 return false;
646             }
647
648             ModelCategoryEntry defaultPropertyCategory;
649             PropertyEntry defaultProperty = FindPropertyEntry(defaultPropertyName, out defaultPropertyCategory);
650             CategoryEditor defaultCategoryEditor = FindCategoryEditor(defaultProperty, defaultPropertyCategory);
651             UIElement element = null;
652             bool elementPendingGeneration = false;
653
654             // Try to look up the correct UIElement to select
655             //
656             if (defaultCategoryEditor != null) 
657             {
658                 element = FindCategoryEditorVisual(defaultCategoryEditor, defaultPropertyCategory, out elementPendingGeneration);
659             }
660             else if (defaultProperty != null) 
661             {
662                 element = FindPropertyEntryVisual(defaultProperty, defaultPropertyCategory, out elementPendingGeneration);
663             }
664             else if (this.Count > 0) 
665             {
666                 // Nothing found, so select the first selectable thing in the list
667                 element = PropertySelection.FindNeighborSelectionStop<UIElement>(this, SearchDirection.Next);
668                 elementPendingGeneration = false;
669             }
670
671             // If the UIElement was found, select it.  Otherwise, if it should exist but it wasn't generated yet,
672             // wait until it is and try again.
673             //
674             if (element != null) 
675             {
676
677                 SelectAndFocus(element, StealFocusMode.OnlyIfCategoryListHasFocusWithin);
678
679                 // Ensure that we are in Default SelectionMode because calling SelectAndFocus automatically switches us
680                 // to Sticky mode
681                 //
682                 ResetSelectionMode();
683
684             }
685             else if (elementPendingGeneration && firstTime) 
686             {
687
688                 // Set the firstTime flag to false, to prevent any infinite loops should things go wrong
689                 this.Dispatcher.BeginInvoke(
690                     DispatcherPriority.Loaded,
691                     new SelectDefaultPropertyHelperDelegate(SelectDefaultPropertyHelper),
692                     defaultPropertyName,
693                     false);
694
695             }
696             else if (elementPendingGeneration && !firstTime) 
697             {
698                 elementPendingGeneration = false;
699             }
700
701             return element != null || elementPendingGeneration;
702         }
703
704         // Find the closest IsSelectable parent, select it and set focus on it.
705         //
706         private void SelectAndFocus(DependencyObject element, StealFocusMode focusMode) 
707         {
708             Select(element);
709             SynchronizeSelectionFocus(focusMode);
710         }
711
712         // Find the closest IsSelectable parent and select it.  Don't mess with focus.
713         //
714         private void Select(DependencyObject visualSource) 
715         {
716             if (visualSource != null) 
717             {
718                 FrameworkElement selection = PropertySelection.FindParentSelectionStop<FrameworkElement>(visualSource);
719
720                 if (selection != _selection) 
721                 {
722
723                     // Unselect anything that was selected previously
724                     if (_selection != null) 
725                     {
726                         PropertySelection.SetIsSelected(_selection, false);
727                     }
728
729                     _selection = selection;
730
731                     // Select whatever we need to select now
732                     if (_selection != null) 
733                     {
734                         PropertySelection.SetIsSelected(_selection, true);
735
736                         // Bring the full PropertyContainer into view, if one exists
737                         FrameworkElement focusableElement = VisualTreeUtils.FindFocusableElement<FrameworkElement>(_selection) ?? _selection;
738                         FrameworkElement parentPropertyContainer = VisualTreeUtils.FindVisualAncestor<PropertyContainer>(focusableElement);
739                         FrameworkElement bringIntoViewElement = parentPropertyContainer ?? focusableElement;
740
741                         bringIntoViewElement.BringIntoView();
742
743                         // As soon as the user manually selects a property, automatically switch to Sticky mode
744                         _selectionMode = PropertySelectionMode.Sticky;
745                     }
746                 }
747             }
748         }
749
750
751         // Keyboard Navigation
752
753         protected override void OnKeyDown(KeyEventArgs e) 
754         {
755
756             // Intercept Up, Down, Left, Right key strokes and use them to navigate around the
757             // the control
758             //
759             if (e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up || e.Key == Key.Down) 
760             {
761                 if (_selection != null && !e.Handled) 
762                 {
763                     if (object.Equals(
764                         Keyboard.FocusedElement,
765                         VisualTreeUtils.FindFocusableElement<FrameworkElement>(_selection))) 
766                     {
767
768                         ISelectionStop selectionStop = PropertySelection.GetSelectionStop(_selection);
769                         if (selectionStop != null) 
770                         {
771
772                             if (selectionStop.IsExpandable) 
773                             {
774                                 if ((e.Key == Key.Right && !selectionStop.IsExpanded) ||
775                                     (e.Key == Key.Left && selectionStop.IsExpanded)) 
776                                 {
777
778                                     selectionStop.IsExpanded = !selectionStop.IsExpanded;
779                                     e.Handled = true;
780                                 }
781                             }
782
783                         }
784
785                         if (!e.Handled) 
786                         {
787
788                             SearchDirection direction = e.Key == Key.Up || e.Key == Key.Left ? SearchDirection.Previous : SearchDirection.Next;
789                             FrameworkElement nextStop = PropertySelection.FindNeighborSelectionStop<FrameworkElement>(_selection, direction);
790
791                             // If need to select something, select it
792                             if (nextStop != null) 
793                             {
794                                 SelectAndFocus(nextStop, StealFocusMode.Always);
795                             }
796
797                             e.Handled = true;
798                         }
799                     }
800                 }
801             }
802
803             base.OnKeyDown(e);
804         }
805
806         // SharedPropertyValueColumnWidth State Logic
807
808         protected override Size ArrangeOverride(Size arrangeBounds) 
809         {
810
811             // Set the content width, rather than the entire container width
812             object gridLength = this.FindResource("OpenCloseColumnGridLength");
813             if (gridLength != null && gridLength is GridLength)
814             {
815                 _sharedWidthContainer.ContainerWidth = arrangeBounds.Width - ((GridLength)gridLength).Value;
816             }
817             else
818             {
819                 _sharedWidthContainer.ContainerWidth = arrangeBounds.Width;
820             }
821
822             return base.ArrangeOverride(arrangeBounds);
823         }
824
825         // Visual Lookup Helpers
826
827         // <summary>
828         // Looks for and returns the PropertyContainer used to represent the specified PropertyEntry.  Returns
829         // null if not found.
830         // </summary>
831         // <param name="property">PropertyEntry to look up</param>
832         // <param name="parentCategory">Category to examine</param>
833         // <param name="pendingGeneration">Set to true if the specified property exists in a collapsed container
834         // (CategoryContainer or an advanced section).  If so, the section is expanded, but the visual does not
835         // exist yet and should be requested later.
836         // </param>
837         // <returns>PropertyContainer for the specified PropertyEntry if found, null otherwise</returns>
838         internal PropertyContainer FindPropertyEntryVisual(PropertyEntry property, CategoryEntry parentCategory, out bool pendingGeneration) 
839         {
840             pendingGeneration = false;
841
842             if (property == null || parentCategory == null)
843             {
844                 return null;
845             }
846
847             if (property.MatchesFilter == false)
848             {
849                 return null;
850             }
851
852             CiderCategoryContainer categoryContainer = this.ItemContainerGenerator.ContainerFromItem(parentCategory) as CiderCategoryContainer;
853             if (categoryContainer == null)
854             {
855                 return null;
856             }
857
858             // Expand the parent category, if it isn't already
859             if (!categoryContainer.Expanded) 
860             {
861                 categoryContainer.Expanded = true;
862                 pendingGeneration = true;
863             }
864
865             // Expand the parent advanced section, if any and if it isn't already
866             if (property.IsAdvanced && !categoryContainer.AdvancedSectionPinned) 
867             {
868                 categoryContainer.AdvancedSectionPinned = true;
869                 pendingGeneration = true;
870             }
871
872             bool pendingGenerationTemp;
873             PropertyContainer propertyContainer = categoryContainer.ContainerFromProperty(property, out pendingGenerationTemp);
874             pendingGeneration |= pendingGenerationTemp;
875
876             if (propertyContainer != null)
877             {
878                 pendingGeneration = false;
879             }
880
881             return propertyContainer;
882         }
883
884         // <summary>
885         // Looks for and returns CategoryContainer for the specified CategoryEntry.  Returns null if not
886         // found.
887         // </summary>
888         // <param name="category">CategoryEntry to look for.</param>
889         // <returns>Corresponding CategoryContainer if found, null otherwise.</returns>
890         internal CategoryContainer FindCategoryEntryVisual(CategoryEntry category) 
891         {
892             if (category == null)
893             {
894                 return null;
895             }
896
897             CiderCategoryContainer categoryContainer = this.ItemContainerGenerator.ContainerFromItem(category) as CiderCategoryContainer;
898             return categoryContainer;
899         }
900
901         // <summary>
902         // Looks for and returns the UIElement used to represent the specified CategoryEditor. Returns
903         // null if not found.
904         // </summary>
905         // <param name="editor">CategoryEditor to look for.</param>
906         // <param name="category">Category to look in.</param>
907         // <param name="pendingGeneration">Set to true if the specified editor exists in a collapsed container
908         // (CategoryContainer or an advanced section).  If so, the section is expanded, but the visual does not
909         // exist yet and should be requested later.</param>
910         // <returns>UIElement for the specified CategoryEditor if found, null otherwise</returns>
911         internal UIElement FindCategoryEditorVisual(CategoryEditor editor, ModelCategoryEntry category, out bool pendingGeneration) 
912         {
913             pendingGeneration = false;
914
915             if (editor == null || category == null)
916             {
917                 return null;
918             }
919
920             UIElement editorVisual = null;
921
922             CiderCategoryContainer categoryContainer = this.ItemContainerGenerator.ContainerFromItem(category) as CiderCategoryContainer;
923             if (categoryContainer == null)
924             {
925                 return null;
926             }
927
928             // Expand the parent category, if it isn't already
929             if (!categoryContainer.Expanded) 
930             {
931                 categoryContainer.Expanded = true;
932                 pendingGeneration = true;
933             }
934
935             // Expand the parent advanced section, if any and if it isn't already
936             if (!categoryContainer.AdvancedSectionPinned && ExtensibilityAccessor.GetIsAdvanced(editor)) 
937             {
938                 categoryContainer.AdvancedSectionPinned = true;
939                 pendingGeneration = true;
940             }
941
942             bool pendingGenerationTemp;
943             editorVisual = categoryContainer.ContainerFromEditor(editor, out pendingGenerationTemp);
944             pendingGeneration |= pendingGenerationTemp;
945
946             if (editorVisual != null)
947             {
948                 pendingGeneration = false;
949             }
950
951             return editorVisual;
952         }
953
954         // Logical Lookup Helpers
955
956         // <summary>
957         // Looks for a CategoryEntry contained in this list with the given name
958         // </summary>
959         // <param name="name">Name to look for</param>
960         // <returns>CategoryEntry with the specified name if found, null otherwise</returns>
961         internal CategoryEntry FindCategory(string name) 
962         {
963             if (name == null)
964             {
965                 throw FxTrace.Exception.ArgumentNull("name");
966             }
967
968             foreach (CategoryEntry category in this.Items) 
969             {
970                 if (category.CategoryName.Equals(name))
971                 {
972                     return category;
973                 }
974             }
975
976             return null;
977         }
978
979         // <summary>
980         // Looks for the PropertyEntry and its parent CategoryEntry for the specified property
981         // </summary>
982         // <param name="propertyName">Property to look for</param>
983         // <param name="parentCategory">Parent CategoryEntry of the given property</param>
984         // <returns>Corresponding PropertyEntry if found, null otherwise.</returns>
985         internal PropertyEntry FindPropertyEntry(string propertyName, out ModelCategoryEntry parentCategory) 
986         {
987             parentCategory = null;
988             if (propertyName == null)
989             {
990                 return null;
991             }
992
993             foreach (ModelCategoryEntry category in this.Items) 
994             {
995                 PropertyEntry property = category[propertyName];
996                 if (property != null) 
997                 {
998                     parentCategory = category;
999                     return property;
1000                 }
1001             }
1002
1003             return null;
1004         }
1005
1006         // <summary>
1007         // Looks for the CategoryEditor of the specified type and returns it if found.
1008         // </summary>
1009         // <param name="editorTypeName">Type name of the editor to look for.</param>
1010         // <param name="category">CategoryEntry that the editor belongs to, if found.</param>
1011         // <returns>CategoryEditor instance of the given type name if found, null otherwise.</returns>
1012         internal CategoryEditor FindCategoryEditor(string editorTypeName, out ModelCategoryEntry category) 
1013         {
1014             category = null;
1015             if (string.IsNullOrEmpty(editorTypeName) || this.Items == null)
1016             {
1017                 return null;
1018             }
1019
1020             foreach (ModelCategoryEntry currentCategory in this.Items) 
1021             {
1022                 foreach (CategoryEditor editor in currentCategory.CategoryEditors) 
1023                 {
1024                     if (string.Equals(editorTypeName, editor.GetType().Name)) 
1025                     {
1026                         category = currentCategory;
1027                         return editor;
1028                     }
1029                 }
1030             }
1031
1032             return null;
1033         }
1034
1035         // Find the first CategoryEditor that consumes the specified property in the specified category.
1036         //
1037         private static CategoryEditor FindCategoryEditor(PropertyEntry property, ModelCategoryEntry parentCategory) 
1038         {
1039             if (property == null || parentCategory == null)
1040             {
1041                 return null;
1042             }
1043
1044             foreach (CategoryEditor editor in parentCategory.CategoryEditors) 
1045             {
1046                 if (editor.ConsumesProperty(property))
1047                 {
1048                     return editor;
1049                 }
1050             }
1051
1052             return null;
1053         }
1054
1055
1056         // IEnumerable<CategoryBase> Members
1057
1058         public IEnumerator<CategoryBase> GetEnumerator() 
1059         {
1060             foreach (CategoryBase categoryBase in this.Items)
1061             {
1062                 yield return categoryBase;
1063             }
1064         }
1065
1066         // IEnumerable Members
1067
1068         System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
1069         {
1070             return this.GetEnumerator();
1071         }
1072
1073
1074         // INotifyPropertyChanged Members
1075
1076         private void OnPropertyChanged(string propertyName) 
1077         {
1078             if (PropertyChanged != null)
1079             {
1080                 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
1081             }
1082         }
1083
1084         private delegate void MethodInvoker();
1085
1086         // Delegate for SelectDefaultPropertyHelper method so that we can invoke it on Render priority if the
1087         // appropriate UIElements have not been generated yet
1088         //
1089         private delegate bool SelectDefaultPropertyHelperDelegate(string defaultPropertyName, bool firstTime);
1090
1091         // Enum that defines how and when we steal keyboard focus from the rest of the application
1092         // when the property selection changes
1093         //
1094         private enum StealFocusMode 
1095         {
1096             Always,
1097             OnlyIfCategoryListHasFocusWithin,
1098             OnlyIfCurrentSelectionDoesNotHaveFocusWithin
1099         }
1100
1101         // Manages state specific to this CategoryList instance
1102         private class CategoryListStateContainer : IStateContainer 
1103         {
1104
1105             private CategoryList _parent;
1106             private IStateContainer _containers;
1107
1108             public CategoryListStateContainer(CategoryList parent) 
1109             {
1110                 if (parent == null) 
1111                 {
1112                     throw FxTrace.Exception.ArgumentNull("parent");
1113                 }
1114                 _parent = parent;
1115             }
1116
1117             private IStateContainer Containers 
1118             {
1119                 get {
1120                     if (_containers == null)
1121                     {
1122                         _containers = new AggregateStateContainer(
1123                             new FilterStringStateContainer(_parent),
1124                             new PropertyValueWidthStateContainer(_parent));
1125                     }
1126
1127                     return _containers;
1128                 }
1129             }
1130
1131             public object RetrieveState() 
1132             {
1133                 return Containers.RetrieveState();
1134             }
1135
1136             public void RestoreState(object state) 
1137             {
1138                 Containers.RestoreState(state);
1139             }
1140
1141             // FilterStringStateContainer
1142
1143             // StateContainer responsible for the FilterString
1144             //
1145             private class FilterStringStateContainer : IStateContainer 
1146             {
1147                 private CategoryList _parent;
1148
1149                 public FilterStringStateContainer(CategoryList parent) 
1150                 {
1151                     if (parent == null) 
1152                     {
1153                         throw FxTrace.Exception.ArgumentNull("parent");
1154                     }
1155                     _parent = parent;
1156                 }
1157
1158                 public object RetrieveState() 
1159                 {
1160                     return _parent.FilterString;
1161                 }
1162
1163                 public void RestoreState(object state) 
1164                 {
1165                     string filterString = state as string;
1166                     if (!string.IsNullOrEmpty(filterString))
1167                     {
1168                         _parent.FilterString = filterString;
1169                     }
1170                 }
1171             }
1172
1173             // PropertyValueWidthStateContainer
1174
1175             // StateContainer responsible for the width of the property value column
1176             //
1177             private class PropertyValueWidthStateContainer : IStateContainer 
1178             {
1179                 private CategoryList _parent;
1180
1181                 public PropertyValueWidthStateContainer(CategoryList parent) 
1182                 {
1183                     if (parent == null) 
1184                     {
1185                         throw FxTrace.Exception.ArgumentNull("parent");
1186                     }
1187                     _parent = parent;
1188                 }
1189
1190                 public object RetrieveState() 
1191                 {
1192                     return _parent._sharedWidthContainer.ValueColumnPercentage.ToString(CultureInfo.InvariantCulture);
1193                 }
1194
1195                 public void RestoreState(object state) 
1196                 {
1197                     string stateString = state as string;
1198                     if (stateString == null)
1199                     {
1200                         return;
1201                     }
1202
1203                     double percentage;
1204                     if (!double.TryParse(stateString, NumberStyles.Float, CultureInfo.InvariantCulture, out percentage)) 
1205                     {
1206                         Debug.Fail("Invalid PI state: " + stateString);
1207                         return;
1208                     }
1209
1210                     if (percentage >= 0 && percentage <= 1) 
1211                     {
1212                         _parent._sharedWidthContainer.ValueColumnPercentage = percentage;
1213                     }
1214                     else 
1215                     {
1216                         Debug.Fail("Invalid percentage width stored in PI state: " + percentage.ToString(CultureInfo.InvariantCulture));
1217                     }
1218                 }
1219             }
1220
1221        }
1222
1223     }
1224 }