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/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.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/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.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; - } - } - } - } } } 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..44204281fa --- /dev/null +++ b/src/Perspex.Diagnostics/DevTools.paml @@ -0,0 +1,18 @@ + + + + + + + + + + + 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..3b96ebcf5a --- /dev/null +++ b/src/Perspex.Diagnostics/DevTools.paml.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +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 + { + private static Dictionary s_open = new Dictionary(); + + public DevTools(IControl root) + { + InitializeComponent(); + Root = root; + DataContext = new DevToolsViewModel(root); + Root.PointerMoved += RootPointerMoved; + } + + public IControl Root { get; } + + 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) + { + var window = (Window)sender; + var devToolsWindow = default(Window); + + if (s_open.TryGetValue(window, out devToolsWindow)) + { + devToolsWindow.Activate(); + } + else + { + devToolsWindow = new Window + { + Width = 1024, + Height = 512, + Content = new DevTools(window), + DataTemplates = new DataTemplates + { + new ViewLocator(), + } + }; + + 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); + } + + 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/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 +