From 9e8fec942e047d6294bde764f360e47e51573a88 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 16 Jan 2016 13:26:02 +0100 Subject: [PATCH 1/7] Log invalid binding values in PriorityValue. --- src/Perspex.Base/PerspexObject.cs | 6 ++++- src/Perspex.Base/PriorityValue.cs | 22 ++++++++++++++++--- .../Perspex.Base.UnitTests.csproj | 8 +++++++ tests/Perspex.Base.UnitTests/packages.config | 1 + 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 25e0ff48be..b95758a7e7 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -775,7 +775,11 @@ namespace Perspex validate2 = v => validate(this, v); } - PriorityValue result = new PriorityValue(property.Name, property.PropertyType, validate2); + PriorityValue result = new PriorityValue( + property.Name, + property.PropertyType, + validate2, + _propertyLog); result.Changed.Subscribe(x => { diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index 0bbdcc389e..e45eba172f 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Reactive.Subjects; using System.Text; using Perspex.Utilities; +using Serilog; namespace Perspex { @@ -54,19 +55,30 @@ namespace Perspex /// private readonly Func _validate; + /// + /// An optional logger. + /// + private ILogger _logger; + /// /// Initializes a new instance of the class. /// /// The name of the property. /// The value type. /// An optional validation function. - public PriorityValue(string name, Type valueType, Func validate = null) + /// An optional logger + public PriorityValue( + string name, + Type valueType, + Func validate = null, + ILogger logger = null) { _name = name; _valueType = valueType; _value = PerspexProperty.UnsetValue; ValuePriority = int.MaxValue; _validate = validate; + _logger = logger; } /// @@ -225,9 +237,13 @@ namespace Perspex _value = value; _changed.OnNext(Tuple.Create(old, _value)); } - else + else if (_logger != null) { - // TODO: Log error. + _logger.Error( + "Binding produced invalid value for {$Type} {$Property}: {$Value}", + _valueType, + _name, + value); } } diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index 078613a34d..be37a88865 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -40,6 +40,14 @@ 4 + + ..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll + True + + + ..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll + True + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll diff --git a/tests/Perspex.Base.UnitTests/packages.config b/tests/Perspex.Base.UnitTests/packages.config index b87bf5fff7..00922fa4e2 100644 --- a/tests/Perspex.Base.UnitTests/packages.config +++ b/tests/Perspex.Base.UnitTests/packages.config @@ -3,6 +3,7 @@ + From 5773704f2d7fc97b2331ad50850536fb9b5c8d06 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 19 Jan 2016 20:27:55 +0100 Subject: [PATCH 2/7] Started updating DevTools to use XAML. --- samples/ControlCatalog/App.paml.cs | 12 ++ samples/ControlCatalog/ControlCatalog.csproj | 9 ++ samples/ControlCatalog/packages.config | 4 + .../Templates/TreeDataTemplate.cs | 9 +- src/Perspex.Base/PriorityValue.cs | 17 ++- .../Generators/TreeItemContainerGenerator.cs | 2 +- src/Perspex.Diagnostics/DevTools.cs | 120 ------------------ src/Perspex.Diagnostics/DevTools.paml | 17 +++ src/Perspex.Diagnostics/DevTools.paml.cs | 52 ++++++++ .../Perspex.Diagnostics.csproj | 27 +++- src/Perspex.Diagnostics/ViewLocator.cs | 2 +- .../ViewModels/DevToolsViewModel.cs | 57 +++++---- .../ViewModels/LogicalTreeViewModel.cs | 34 ----- .../ViewModels/TreeNode.cs | 7 + ...lTreeViewModel.cs => TreePageViewModel.cs} | 13 +- .../Views/LogicalTreeView.cs | 99 --------------- .../Views/{TreePage.cs => TreePage.paml.cs} | 18 ++- .../Views/TreePageView.paml | 24 ++++ .../Views/VisualTreeView.cs | 101 --------------- 19 files changed, 216 insertions(+), 408 deletions(-) create mode 100644 samples/ControlCatalog/packages.config delete mode 100644 src/Perspex.Diagnostics/DevTools.cs create mode 100644 src/Perspex.Diagnostics/DevTools.paml create mode 100644 src/Perspex.Diagnostics/DevTools.paml.cs delete mode 100644 src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs rename src/Perspex.Diagnostics/ViewModels/{VisualTreeViewModel.cs => TreePageViewModel.cs} (71%) delete mode 100644 src/Perspex.Diagnostics/Views/LogicalTreeView.cs rename src/Perspex.Diagnostics/Views/{TreePage.cs => TreePage.paml.cs} (77%) create mode 100644 src/Perspex.Diagnostics/Views/TreePageView.paml delete mode 100644 src/Perspex.Diagnostics/Views/VisualTreeView.cs diff --git a/samples/ControlCatalog/App.paml.cs b/samples/ControlCatalog/App.paml.cs index b1aa671215..8a49ae20f1 100644 --- a/samples/ControlCatalog/App.paml.cs +++ b/samples/ControlCatalog/App.paml.cs @@ -5,6 +5,7 @@ using Perspex.Controls; using Perspex.Diagnostics; using Perspex.Markup.Xaml; using Perspex.Themes.Default; +using Serilog; namespace ControlCatalog { @@ -14,6 +15,7 @@ namespace ControlCatalog { RegisterServices(); InitializeSubsystems(GetPlatformId()); + InitializeLogging(); Styles = new DefaultTheme(); InitializeComponent(); } @@ -38,6 +40,16 @@ namespace ControlCatalog PerspexXamlLoader.Load(this); } + private void InitializeLogging() + { +#if DEBUG + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Error() + .WriteTo.Trace(outputTemplate: "{Message}") + .CreateLogger(); +#endif + } + private int GetPlatformId() { var args = Environment.GetCommandLineArgs(); diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 1a26286147..d3bf036619 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -36,6 +36,14 @@ + + ..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll + True + + + ..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll + True + @@ -93,6 +101,7 @@ Designer + diff --git a/samples/ControlCatalog/packages.config b/samples/ControlCatalog/packages.config new file mode 100644 index 0000000000..76c9b4d2a5 --- /dev/null +++ b/samples/ControlCatalog/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index 0ff1ec7a6d..ceabd09bb7 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Reactive.Linq; +using System.Reflection; using Perspex.Controls; using Perspex.Controls.Templates; using Perspex.Data; @@ -27,10 +28,12 @@ namespace Perspex.Markup.Xaml.Templates { if (DataType == null) { - throw new InvalidOperationException("DataTemplate must have a DataType."); + return true; + } + else + { + return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); } - - return DataType == data.GetType(); } public IEnumerable ItemsSelector(object item) diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index e45eba172f..b68fae1869 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -224,26 +224,29 @@ namespace Perspex /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - if (TypeUtilities.TryCast(_valueType, value, out value)) + object castValue; + + if (TypeUtilities.TryCast(_valueType, value, out castValue)) { var old = _value; - if (_validate != null && value != PerspexProperty.UnsetValue) + if (_validate != null && castValue != PerspexProperty.UnsetValue) { - value = _validate(value); + castValue = _validate(castValue); } ValuePriority = priority; - _value = value; + _value = castValue; _changed.OnNext(Tuple.Create(old, _value)); } else if (_logger != null) { _logger.Error( - "Binding produced invalid value for {$Type} {$Property}: {$Value}", - _valueType, + "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", _name, - value); + _valueType, + value, + value.GetType()); } } diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index e630924a97..046d018321 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -77,7 +77,7 @@ namespace Perspex.Controls.Generators result.SetValue(ContentProperty, template.Build(item)); result.SetValue(ItemsProperty, template.ItemsSelector(item)); - result.SetValue(IsExpandedProperty, template.IsExpanded(item)); + //result.SetValue(IsExpandedProperty, template.IsExpanded(item)); if (!(item is IControl)) { diff --git a/src/Perspex.Diagnostics/DevTools.cs b/src/Perspex.Diagnostics/DevTools.cs deleted file mode 100644 index a9364d2566..0000000000 --- a/src/Perspex.Diagnostics/DevTools.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Linq; -using Perspex.Controls; -using Perspex.Diagnostics.ViewModels; -using Perspex.Input; -using Perspex.Themes.Default; -using ReactiveUI; - -namespace Perspex.Diagnostics -{ - public class DevTools : Decorator - { - public static readonly PerspexProperty RootProperty = - PerspexProperty.Register("Root"); - - private readonly DevToolsViewModel _viewModel; - - public DevTools() - { - _viewModel = new DevToolsViewModel(); - this.GetObservable(RootProperty).Subscribe(x => _viewModel.Root = x); - - InitializeComponent(); - } - - public Control Root - { - get { return GetValue(RootProperty); } - set { SetValue(RootProperty, value); } - } - - public static IDisposable Attach(Window window) - { - return window.AddHandler( - KeyDownEvent, - WindowPreviewKeyDown, - Interactivity.RoutingStrategies.Tunnel); - } - - private static void WindowPreviewKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.F12) - { - Window window = new Window - { - Width = 1024, - Height = 512, - Content = new DevTools - { - Root = (Window)sender, - }, - }; - - window.Show(); - } - } - - private void InitializeComponent() - { - DataTemplates.Add(new ViewLocator()); - Styles.Add(new DefaultTheme()); - - Child = new Grid - { - RowDefinitions = new RowDefinitions("*,Auto"), - Children = new Controls.Controls - { - new TabControl - { - Items = new[] - { - new TabItem - { - Header = "Logical Tree", - [!ContentControl.ContentProperty] = _viewModel.WhenAnyValue(x => x.LogicalTree), - }, - new TabItem - { - Header = "Visual Tree", - [!ContentControl.ContentProperty] = _viewModel.WhenAnyValue(x => x.VisualTree), - } - }, - }, - new StackPanel - { - Orientation = Orientation.Horizontal, - Gap = 4, - [Grid.RowProperty] = 1, - Children = new Controls.Controls - { - new TextBlock - { - Text = "Focused: " - }, - new TextBlock - { - [!TextBlock.TextProperty] = _viewModel - .WhenAnyValue(x => x.FocusedControl) - .Select(x => x?.GetType().Name ?? "(null)") - }, - new TextBlock - { - Text = "Pointer Over: " - }, - new TextBlock - { - [!TextBlock.TextProperty] = _viewModel - .WhenAnyValue(x => x.PointerOverElement) - .Select(x => x?.GetType().Name ?? "(null)") - } - } - } - } - }; - } - } -} diff --git a/src/Perspex.Diagnostics/DevTools.paml b/src/Perspex.Diagnostics/DevTools.paml new file mode 100644 index 0000000000..76f58222e1 --- /dev/null +++ b/src/Perspex.Diagnostics/DevTools.paml @@ -0,0 +1,17 @@ + + + + + + + + + + + Focused: + + Pointer Over: + + + + \ No newline at end of file diff --git a/src/Perspex.Diagnostics/DevTools.paml.cs b/src/Perspex.Diagnostics/DevTools.paml.cs new file mode 100644 index 0000000000..8b607a496f --- /dev/null +++ b/src/Perspex.Diagnostics/DevTools.paml.cs @@ -0,0 +1,52 @@ +using System; +using Perspex.Controls; +using Perspex.Controls.Templates; +using Perspex.Diagnostics.ViewModels; +using Perspex.Input; +using Perspex.Interactivity; +using Perspex.Markup.Xaml; +using ReactiveUI; + +namespace Perspex.Diagnostics +{ + public class DevTools : UserControl + { + public DevTools(IControl root) + { + this.InitializeComponent(); + this.DataContext = new DevToolsViewModel(root); + } + + public static IDisposable Attach(Window window) + { + return window.AddHandler( + KeyDownEvent, + WindowPreviewKeyDown, + RoutingStrategies.Tunnel); + } + + private static void WindowPreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.F12) + { + Window window = new Window + { + Width = 1024, + Height = 512, + Content = new DevTools((IControl)sender), + DataTemplates = new DataTemplates + { + new ViewLocator(), + } + }; + + window.Show(); + } + } + + private void InitializeComponent() + { + PerspexXamlLoader.Load(this); + } + } +} diff --git a/src/Perspex.Diagnostics/Perspex.Diagnostics.csproj b/src/Perspex.Diagnostics/Perspex.Diagnostics.csproj index 17db490d9b..e36079a39e 100644 --- a/src/Perspex.Diagnostics/Perspex.Diagnostics.csproj +++ b/src/Perspex.Diagnostics/Perspex.Diagnostics.csproj @@ -40,6 +40,14 @@ + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Perspex.Markup.Xaml + + + {6417e941-21bc-467b-a771-0de389353ce6} + Perspex.Markup + {D211E587-D8BC-45B9-95A4-F297C8FA5200} Perspex.Animation @@ -86,23 +94,24 @@ Properties\SharedAssemblyInfo.cs - - - + + + TreePageView.paml + + + DevTools.paml + - - - @@ -124,6 +133,12 @@ + + Designer + + + Designer + + + + + + + \ No newline at end of file diff --git a/src/Perspex.Diagnostics/Views/VisualTreeView.cs b/src/Perspex.Diagnostics/Views/VisualTreeView.cs deleted file mode 100644 index d8d6ca03ce..0000000000 --- a/src/Perspex.Diagnostics/Views/VisualTreeView.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Reactive.Linq; -using Perspex.Controls; -using Perspex.Controls.Templates; -using Perspex.Diagnostics.ViewModels; -using Perspex.Media; -using ReactiveUI; - -namespace Perspex.Diagnostics.Views -{ - using Controls = Controls.Controls; - - internal class VisualTreeView : TreePage - { - private static readonly PerspexProperty ViewModelProperty = - PerspexProperty.Register("ViewModel"); - - public VisualTreeView() - { - InitializeComponent(); - this.GetObservable(DataContextProperty) - .Subscribe(x => ViewModel = (VisualTreeViewModel)x); - } - - public VisualTreeViewModel ViewModel - { - get { return GetValue(ViewModelProperty); } - private set { SetValue(ViewModelProperty, value); } - } - - private void InitializeComponent() - { - TreeView tree; - - Content = new Grid - { - ColumnDefinitions = new ColumnDefinitions - { - new ColumnDefinition(1, GridUnitType.Star), - new ColumnDefinition(4, GridUnitType.Pixel), - new ColumnDefinition(3, GridUnitType.Star), - }, - Children = new Controls - { - (tree = new TreeView - { - DataTemplates = new DataTemplates - { - new FuncTreeDataTemplate(GetHeader, x => x.Children), - }, - [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes), - }), - new GridSplitter - { - Width = 4, - Orientation = Orientation.Vertical, - [Grid.ColumnProperty] = 1, - }, - new ContentControl - { - [!ContentProperty] = this.WhenAnyValue(x => x.ViewModel.Details), - [Grid.ColumnProperty] = 2, - } - } - }; - - tree.GetObservable(TreeView.SelectedItemProperty) - .OfType() - .Subscribe(x => ViewModel.SelectedNode = x); - } - - private Control GetHeader(VisualTreeNode node) - { - var result = new StackPanel - { - Orientation = Orientation.Horizontal, - Gap = 8, - Children = new Controls - { - new TextBlock - { - FontStyle = node.IsInTemplate ? FontStyle.Italic : FontStyle.Normal, - Text = node.Type, - }, - new TextBlock - { - [!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes), - } - } - }; - - result.PointerEnter += AddAdorner; - result.PointerLeave += RemoveAdorner; - - return result; - } - } -} From d69c283767d2eb3d0179e72658d72c15ee1dfd27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 19:38:38 +0100 Subject: [PATCH 3/7] Display Focused and PointerOver control. --- src/Perspex.Diagnostics/DevTools.paml | 1 + .../ViewModels/DevToolsViewModel.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Perspex.Diagnostics/DevTools.paml b/src/Perspex.Diagnostics/DevTools.paml index 76f58222e1..44204281fa 100644 --- a/src/Perspex.Diagnostics/DevTools.paml +++ b/src/Perspex.Diagnostics/DevTools.paml @@ -10,6 +10,7 @@ Focused: + Pointer Over: diff --git a/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs index 67fcc505f4..ef9dc5a371 100644 --- a/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -21,9 +21,9 @@ namespace Perspex.Diagnostics.ViewModels private TreePageViewModel _visualTree; - private readonly ObservableAsPropertyHelper _focusedControl; + private readonly ObservableAsPropertyHelper _focusedControl; - private readonly ObservableAsPropertyHelper _pointerOverElement; + private readonly ObservableAsPropertyHelper _pointerOverElement; public DevToolsViewModel(IControl root) { @@ -46,12 +46,12 @@ namespace Perspex.Diagnostics.ViewModels _focusedControl = KeyboardDevice.Instance .WhenAnyValue(x => x.FocusedElement) + .Select(x => x?.GetType().Name) .ToProperty(this, x => x.FocusedControl); - //_pointerOverElement = this.WhenAnyValue(x => x.Root, x => x as TopLevel) - // .Select(x => x?.GetObservable(TopLevel.PointerOverElementProperty) ?? Observable.Empty()) - // .Switch() - // .ToProperty(this, x => x.PointerOverElement); + _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty) + .Select(x => x?.GetType().Name) + .ToProperty(this, x => x.PointerOverElement); } public ReactiveObject Content @@ -66,8 +66,8 @@ namespace Perspex.Diagnostics.ViewModels set { this.RaiseAndSetIfChanged(ref _selectedTab, value); } } - public IInputElement FocusedControl => _focusedControl.Value; + public string FocusedControl => _focusedControl.Value; - //public IInputElement PointerOverElement => _pointerOverElement.Value; + public string PointerOverElement => _pointerOverElement.Value; } } From 7f6b433d8ca47fd1477133cf0b118c0bc25e756b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 19:49:37 +0100 Subject: [PATCH 4/7] Only open 1 devtools per window. --- src/Perspex.Diagnostics/DevTools.paml.cs | 50 ++++++++++++++++++------ 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Perspex.Diagnostics/DevTools.paml.cs b/src/Perspex.Diagnostics/DevTools.paml.cs index 8b607a496f..ab374f09f1 100644 --- a/src/Perspex.Diagnostics/DevTools.paml.cs +++ b/src/Perspex.Diagnostics/DevTools.paml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Perspex.Controls; using Perspex.Controls.Templates; using Perspex.Diagnostics.ViewModels; @@ -11,12 +12,17 @@ namespace Perspex.Diagnostics { public class DevTools : UserControl { + private static Dictionary s_open = new Dictionary(); + public DevTools(IControl root) { - this.InitializeComponent(); - this.DataContext = new DevToolsViewModel(root); + InitializeComponent(); + Root = root; + DataContext = new DevToolsViewModel(root); } + public IControl Root { get; } + public static IDisposable Attach(Window window) { return window.AddHandler( @@ -29,21 +35,43 @@ namespace Perspex.Diagnostics { if (e.Key == Key.F12) { - Window window = new Window + var window = (Window)sender; + var devToolsWindow = default(Window); + + if (s_open.TryGetValue(window, out devToolsWindow)) + { + devToolsWindow.Activate(); + } + else { - Width = 1024, - Height = 512, - Content = new DevTools((IControl)sender), - DataTemplates = new DataTemplates + devToolsWindow = new Window { - new ViewLocator(), - } - }; + Width = 1024, + Height = 512, + Content = new DevTools(window), + DataTemplates = new DataTemplates + { + new ViewLocator(), + } + }; - window.Show(); + devToolsWindow.Closed += DevToolsClosed; + s_open.Add((Window)sender, devToolsWindow); + devToolsWindow.Show(); + } } } + private static void DevToolsClosed(object sender, EventArgs e) + { + var devToolsWindow = (Window)sender; + var devTools = (DevTools)devToolsWindow.Content; + var window = (Window)devTools.Root; + + s_open.Remove(window); + devToolsWindow.Closed -= DevToolsClosed; + } + private void InitializeComponent() { PerspexXamlLoader.Load(this); From 14b349e00667ff8949827d783dc2ebb4df701086 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 20:23:41 +0100 Subject: [PATCH 5/7] Select node when ctrl+shift held down. TreeView selection isn't getting updated though. --- src/Perspex.Diagnostics/DevTools.paml.cs | 12 ++++ .../ViewModels/DevToolsViewModel.cs | 10 +++ .../ViewModels/LogicalTreeNode.cs | 8 +-- .../ViewModels/TreeNode.cs | 17 +++-- .../ViewModels/TreePageViewModel.cs | 69 +++++++++++++++++++ .../ViewModels/VisualTreeNode.cs | 10 +-- .../Views/TreePageView.paml | 4 +- 7 files changed, 113 insertions(+), 17 deletions(-) diff --git a/src/Perspex.Diagnostics/DevTools.paml.cs b/src/Perspex.Diagnostics/DevTools.paml.cs index ab374f09f1..3b96ebcf5a 100644 --- a/src/Perspex.Diagnostics/DevTools.paml.cs +++ b/src/Perspex.Diagnostics/DevTools.paml.cs @@ -19,6 +19,7 @@ namespace Perspex.Diagnostics InitializeComponent(); Root = root; DataContext = new DevToolsViewModel(root); + Root.PointerMoved += RootPointerMoved; } public IControl Root { get; } @@ -76,5 +77,16 @@ namespace Perspex.Diagnostics { PerspexXamlLoader.Load(this); } + + private void RootPointerMoved(object sender, PointerEventArgs e) + { + var modifiers = InputModifiers.Control | InputModifiers.Shift; + + if ((e.InputModifiers & modifiers) == modifiers) + { + var vm = (DevToolsViewModel)DataContext; + vm.SelectControl((IControl)e.Source); + } + } } } diff --git a/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs index ef9dc5a371..475579fdb5 100644 --- a/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs @@ -69,5 +69,15 @@ namespace Perspex.Diagnostics.ViewModels public string FocusedControl => _focusedControl.Value; public string PointerOverElement => _pointerOverElement.Value; + + public void SelectControl(IControl control) + { + var tree = Content as TreePageViewModel; + + if (tree != null) + { + tree.SelectControl(control); + } + } } } diff --git a/src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs index 0d15719128..bef186b246 100644 --- a/src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs @@ -9,16 +9,16 @@ namespace Perspex.Diagnostics.ViewModels { internal class LogicalTreeNode : TreeNode { - public LogicalTreeNode(ILogical logical) - : base((Control)logical) + public LogicalTreeNode(ILogical logical, TreeNode parent) + : base((Control)logical, parent) { - Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x)); + Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x, this)); } public static LogicalTreeNode[] Create(object control) { var logical = control as ILogical; - return logical != null ? new[] { new LogicalTreeNode(logical) } : null; + return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; } } } diff --git a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs index 9c46f5f344..62044d2a28 100644 --- a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs @@ -13,11 +13,12 @@ namespace Perspex.Diagnostics.ViewModels internal class TreeNode : ReactiveObject { private string _classes; - private bool _isExpanded = true; + private bool _isExpanded; - public TreeNode(Control control) + public TreeNode(Control control, TreeNode parent) { Control = control; + Parent = parent; Type = control.GetType().Name; var classesChanged = Observable.FromEventPattern< @@ -53,19 +54,23 @@ namespace Perspex.Diagnostics.ViewModels private set { this.RaiseAndSetIfChanged(ref _classes, value); } } + public Control Control + { + get; + } + public bool IsExpanded { get { return _isExpanded; } - private set { this.RaiseAndSetIfChanged(ref _isExpanded, value); } + set { this.RaiseAndSetIfChanged(ref _isExpanded, value); } } - public string Type + public TreeNode Parent { get; - private set; } - public Control Control + public string Type { get; private set; diff --git a/src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs index 90e1b49492..346969abdc 100644 --- a/src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reactive.Linq; +using Perspex.Controls; +using Perspex.VisualTree; using ReactiveUI; namespace Perspex.Diagnostics.ViewModels @@ -29,5 +31,72 @@ namespace Perspex.Diagnostics.ViewModels } public ControlDetailsViewModel Details => _details.Value; + + public TreeNode FindNode(IControl control) + { + foreach (var node in Nodes) + { + var result = FindNode(node, control); + + if (result != null) + { + return result; + } + } + + return null; + } + + public void SelectControl(IControl control) + { + var node = default(TreeNode); + + while (node == null && control != null) + { + node = FindNode(control); + + if (node == null) + { + control = control.GetVisualParent(); + } + } + + if (node != null) + { + SelectedNode = node; + ExpandNode(node.Parent); + } + } + + private void ExpandNode(TreeNode node) + { + if (node != null) + { + node.IsExpanded = true; + ExpandNode(node.Parent); + } + } + + private TreeNode FindNode(TreeNode node, IControl control) + { + if (node.Control == control) + { + return node; + } + else + { + foreach (var child in node.Children) + { + var result = FindNode(child, control); + + if (result != null) + { + return result; + } + } + } + + return null; + } } } diff --git a/src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs index 64064df20a..f92a51439a 100644 --- a/src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs @@ -9,18 +9,18 @@ namespace Perspex.Diagnostics.ViewModels { internal class VisualTreeNode : TreeNode { - public VisualTreeNode(IVisual visual) - : base((Control)visual) + public VisualTreeNode(IVisual visual, TreeNode parent) + : base((Control)visual, parent) { var host = visual as IVisualTreeHost; if (host?.Root == null) { - Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x)); + Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this)); } else { - Children = new ReactiveList(new[] { new VisualTreeNode(host.Root) }); + Children = new ReactiveList(new[] { new VisualTreeNode(host.Root, this) }); } if (Control != null) @@ -34,7 +34,7 @@ namespace Perspex.Diagnostics.ViewModels public static VisualTreeNode[] Create(object control) { var visual = control as IVisual; - return visual != null ? new[] { new VisualTreeNode(visual) } : null; + return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; } } } diff --git a/src/Perspex.Diagnostics/Views/TreePageView.paml b/src/Perspex.Diagnostics/Views/TreePageView.paml index 4914d1e3dc..ba13d4e263 100644 --- a/src/Perspex.Diagnostics/Views/TreePageView.paml +++ b/src/Perspex.Diagnostics/Views/TreePageView.paml @@ -11,11 +11,11 @@ - + From 69d98efeeb914a7aaf8cd6f3677991c64c59480a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 24 Jan 2016 17:36:05 +0100 Subject: [PATCH 6/7] Highlight hovered control in devtools. --- .../Generators/ItemContainerEventArgs.cs | 13 +++++++++++++ .../Generators/TreeContainerIndex.cs | 19 +++++++++++++++++++ .../Primitives/HeaderedItemsControl.cs | 2 +- .../Views/TreePage.paml.cs | 19 +++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs b/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs index 86e2b890d1..0a9c3544d7 100644 --- a/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs +++ b/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs @@ -12,6 +12,19 @@ namespace Perspex.Controls.Generators /// public class ItemContainerEventArgs : EventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The index of the first container in the source items. + /// The container. + public ItemContainerEventArgs( + int startingIndex, + ItemContainer container) + { + StartingIndex = startingIndex; + Containers = new[] { container }; + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Perspex.Controls/Generators/TreeContainerIndex.cs b/src/Perspex.Controls/Generators/TreeContainerIndex.cs index 9de4ca1050..ab07cb454d 100644 --- a/src/Perspex.Controls/Generators/TreeContainerIndex.cs +++ b/src/Perspex.Controls/Generators/TreeContainerIndex.cs @@ -1,6 +1,7 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; using System.Collections.Generic; namespace Perspex.Controls.Generators @@ -19,6 +20,16 @@ namespace Perspex.Controls.Generators private readonly Dictionary _itemToContainer = new Dictionary(); private readonly Dictionary _containerToItem = new Dictionary(); + /// + /// Signalled whenever new containers are materialized. + /// + public event EventHandler Materialized; + + /// + /// Event raised whenever containers are dematerialized. + /// + public event EventHandler Dematerialized; + /// /// Gets the currently materialized containers. /// @@ -33,6 +44,10 @@ namespace Perspex.Controls.Generators { _itemToContainer.Add(item, container); _containerToItem.Add(container, item); + + Materialized?.Invoke( + this, + new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); } /// @@ -44,6 +59,10 @@ namespace Perspex.Controls.Generators var item = _containerToItem[container]; _containerToItem.Remove(container); _itemToContainer.Remove(item); + + Dematerialized?.Invoke( + this, + new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); } /// diff --git a/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs b/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs index 5f543cae17..47d714e4da 100644 --- a/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs @@ -51,8 +51,8 @@ namespace Perspex.Controls.Primitives /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(e); HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + base.OnTemplateApplied(e); } } } diff --git a/src/Perspex.Diagnostics/Views/TreePage.paml.cs b/src/Perspex.Diagnostics/Views/TreePage.paml.cs index 43c9829a95..3937358ba1 100644 --- a/src/Perspex.Diagnostics/Views/TreePage.paml.cs +++ b/src/Perspex.Diagnostics/Views/TreePage.paml.cs @@ -1,4 +1,5 @@ using Perspex.Controls; +using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Controls.Shapes; using Perspex.Diagnostics.ViewModels; @@ -11,10 +12,12 @@ namespace Perspex.Diagnostics.Views public class TreePageView : UserControl { private Control _adorner; + private TreeView _tree; public TreePageView() { this.InitializeComponent(); + _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; } protected void AddAdorner(object sender, PointerEventArgs e) @@ -46,6 +49,22 @@ namespace Perspex.Diagnostics.Views private void InitializeComponent() { PerspexXamlLoader.Load(this); + _tree = this.FindControl("tree"); + } + + private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e) + { + var item = (TreeViewItem)e.Containers[0].ContainerControl; + item.TemplateApplied += TreeViewItemTemplateApplied; + } + + private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e) + { + var item = (TreeViewItem)sender; + var header = item.HeaderPresenter.Child; + header.PointerEnter += AddAdorner; + header.PointerLeave += RemoveAdorner; + item.TemplateApplied -= TreeViewItemTemplateApplied; } } } From 0faeaf47e34079b7c12184d5b6c5adbe5ad1c5a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 25 Jan 2016 14:41:56 +0100 Subject: [PATCH 7/7] Scroll newly selected tree view node into view. --- src/Perspex.Base/Threading/DispatcherTimer.cs | 27 +++++++++ src/Perspex.Controls/TreeView.cs | 57 ++++++++++++++++++- src/Perspex.Controls/TreeViewItem.cs | 22 +------ 3 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/Perspex.Base/Threading/DispatcherTimer.cs b/src/Perspex.Base/Threading/DispatcherTimer.cs index 376ca5c0c9..1ece331c13 100644 --- a/src/Perspex.Base/Threading/DispatcherTimer.cs +++ b/src/Perspex.Base/Threading/DispatcherTimer.cs @@ -146,6 +146,33 @@ namespace Perspex.Threading return Disposable.Create(() => timer.Stop()); } + /// + /// Runs a method once, after the specified interval. + /// + /// + /// The method to call after the interval has elapsed. + /// + /// The interval after which to call the method. + /// The priority to use. + /// An used to cancel the timer. + public static IDisposable RunOnce( + Action action, + TimeSpan interval, + DispatcherPriority priority = DispatcherPriority.Normal) + { + var timer = new DispatcherTimer(priority) { Interval = interval }; + + timer.Tick += (s, e) => + { + action(); + timer.Stop(); + }; + + timer.Start(); + + return Disposable.Create(() => timer.Stop()); + } + /// /// Starts the timer. /// diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index ea2b5d632c..dd36db7ec0 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -8,6 +8,7 @@ using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Interactivity; using Perspex.Styling; +using Perspex.Threading; using Perspex.VisualTree; namespace Perspex.Controls @@ -17,6 +18,14 @@ namespace Perspex.Controls /// public class TreeView : ItemsControl { + /// + /// Defines the property. + /// + public static readonly PerspexProperty AutoScrollToSelectedItemProperty = + PerspexProperty.Register( + nameof(AutoScrollToSelectedItem), + defaultValue: true); + /// /// Defines the property. /// @@ -41,6 +50,15 @@ namespace Perspex.Controls public new ITreeItemContainerGenerator ItemContainerGenerator => (ITreeItemContainerGenerator)base.ItemContainerGenerator; + /// + /// Gets or sets a value indicating whether to automatically scroll to newly selected items. + /// + public bool AutoScrollToSelectedItem + { + get { return GetValue(AutoScrollToSelectedItemProperty); } + set { SetValue(AutoScrollToSelectedItemProperty, value); } + } + /// /// Gets or sets the selected item. /// @@ -65,6 +83,11 @@ namespace Perspex.Controls { var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem); MarkContainerSelected(container, true); + + if (AutoScrollToSelectedItem && container != null) + { + container.BringIntoView(); + } } } } @@ -72,12 +95,14 @@ namespace Perspex.Controls /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new TreeItemContainerGenerator( + var result = new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty, new TreeContainerIndex()); + result.Index.Materialized += ContainerMaterialized; + return result; } /// @@ -190,6 +215,36 @@ namespace Perspex.Controls return null; } + /// + /// Called when a new item container is materialized, to set its selected state. + /// + /// The event sender. + /// The event args. + private void ContainerMaterialized(object sender, ItemContainerEventArgs e) + { + var selectedItem = SelectedItem; + + if (selectedItem != null) + { + foreach (var container in e.Containers) + { + if (container.Item == selectedItem) + { + ((TreeViewItem)container.ContainerControl).IsSelected = true; + + if (AutoScrollToSelectedItem) + { + DispatcherTimer.RunOnce( + container.ContainerControl.BringIntoView, + TimeSpan.Zero); + } + + break; + } + } + } + } + /// /// Sets a container's 'selected' class or . /// diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index e68a77b56e..e2d8107cfc 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/src/Perspex.Controls/TreeViewItem.cs @@ -73,16 +73,12 @@ namespace Perspex.Controls /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - var result = new TreeItemContainerGenerator( + return new TreeItemContainerGenerator( this, TreeViewItem.HeaderProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty, _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex()); - - result.Materialized += ItemMaterialized; - - return result; } /// @@ -123,21 +119,5 @@ namespace Perspex.Controls base.OnKeyDown(e); } - - private void ItemMaterialized(object sender, ItemContainerEventArgs e) - { - var selectedItem = _treeView?.SelectedItem; - - if (selectedItem != null) - { - foreach (var container in e.Containers) - { - if (container.Item == selectedItem) - { - ((TreeViewItem)container.ContainerControl).IsSelected = true; - } - } - } - } } }