diff --git a/docs/build.md b/docs/build.md index b3be7581c4..935341c0d5 100644 --- a/docs/build.md +++ b/docs/build.md @@ -7,8 +7,11 @@ Perspex requires Visual Studio 2015 to build on Windows. ### Install GTK Sharp To compile the full project under windows, you must have [gtk-sharp](http://www.mono-project.com/download/#download-win) installed. However, if you're -not interested in building the cross-platform bits you can simply unload the Perspex.Cairo and -Perspex.Gtk project in Visual Studio. +not interested in building the cross-platform bits you can simply unload these projects from Visual Studio: + + - Perspex.Cairo + - Perspex.Cairo.RenderTests + - Perspex.Gtk ### Clone the Perspex repository @@ -19,7 +22,7 @@ is linked as a submodule in the git repository, so run: git submodule update --init -The next step is to download the Skia native libraries. Run ```getnatives.ps1``` PowerShell script which can be found under the folder ```Perspex\src\Skia\```. +The next step is to download the Skia native libraries. Run ```getnatives.ps1``` PowerShell script which can be found under the folder ```src\Skia\```. ## Linux @@ -39,14 +42,14 @@ Then install the needed packages: ### Clone the Perspex repository - git clone https://github.com/grokys/Perspex.git + git clone https://github.com/Perspex/Perspex.git We currently need to build our own private version of ReactiveUI as it doesn't work on mono. This is linked as a submodule in the git repository, so run: git submodule update --init -The next step is to download the Skia native libraries. Run ```getnatives.sh``` script which can be found under the folder ```Perspex\src\Skia\```. +The next step is to download the Skia native libraries. Run ```getnatives.sh``` script which can be found under the folder ```src\Skia\```. ### Load the Project in MonoDevelop @@ -57,4 +60,4 @@ Set the TestApplication project as the startup project and click Run. There will be some compile errors in the tests, but ignore them for now. -You can track the Linux version's progress in the [Linux issue](https://github.com/grokys/Perspex/issues/78). +You can track the Linux version's progress in the [Linux issue](https://github.com/Perspex/Perspex/issues/78). 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/DockPanel.cs b/src/Perspex.Controls/DockPanel.cs index a1224a5272..8108a8c3cb 100644 --- a/src/Perspex.Controls/DockPanel.cs +++ b/src/Perspex.Controls/DockPanel.cs @@ -1,445 +1,176 @@ namespace Perspex.Controls { using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using Layout; - public class DockPanel : Panel + /// + /// Defines the available docking modes for a control in a . + /// + public enum Dock { - public static readonly PerspexProperty DockProperty = PerspexProperty.RegisterAttached("Dock"); + Left = 0, + Bottom, + Right, + Top + } + /// + /// A panel which arranges its children at the top, bottom, left, right or center. + /// + public class DockPanel : Panel + { + /// + /// Defines the Dock attached property. + /// + public static readonly PerspexProperty DockProperty = + PerspexProperty.RegisterAttached("Dock"); + + /// + /// Defines the property. + /// + public static readonly PerspexProperty LastChildFillProperty = + PerspexProperty.Register( + nameof(LastChildFillProperty), + defaultValue: true); + + /// + /// Initializes static members of the class. + /// static DockPanel() { AffectsArrange(DockProperty); } - // ReSharper disable once UnusedMember.Global - public static Dock GetDock(PerspexObject perspexObject) + /// + /// Gets the value of the Dock attached property on the specified control. + /// + /// The control. + /// The Dock attached property. + public static Dock GetDock(Control control) { - return perspexObject.GetValue(DockProperty); + return control.GetValue(DockProperty); } - // ReSharper disable once UnusedMember.Global - public static void SetDock(PerspexObject element, Dock dock) + /// + /// Sets the value of the Dock attached property on the specified control. + /// + /// The control. + /// The value of the Dock property. + public static void SetDock(Control control, Dock value) { - element.SetValue(DockProperty, dock); + control.SetValue(DockProperty, value); } - public static readonly PerspexProperty LastChildFillProperty = PerspexProperty.Register(nameof(LastChildFillProperty), defaultValue: true); - + /// + /// Gets or sets a value which indicates whether the last child of the + /// fills the remaining space in the panel. + /// public bool LastChildFill { get { return GetValue(LastChildFillProperty); } set { SetValue(LastChildFillProperty, value); } } - protected override Size MeasureOverride(Size availableSize) - { - if (!LastChildFill) - { - return MeasureItemsThatWillBeDocked(availableSize, Children); - } - - var sizeRequiredByDockingItems = MeasureItemsThatWillBeDocked(availableSize, Children.WithoutLast()); - var elementThatWillFill = Children.Last(); - elementThatWillFill.Measure(availableSize - sizeRequiredByDockingItems); - var finalSize = sizeRequiredByDockingItems.Inflate(new Thickness(elementThatWillFill.DesiredSize.Width, elementThatWillFill.DesiredSize.Height)); - return finalSize; - } - - private static Size MeasureItemsThatWillBeDocked(Size availableSize, IEnumerable children) + /// + protected override Size MeasureOverride(Size constraint) { - var requiredHorizontalLength = 0D; - var requiredVerticalLength = 0D; + double usedWidth = 0.0; + double usedHeight = 0.0; + double maximumWidth = 0.0; + double maximumHeight = 0.0; - foreach (var control in children) + // Measure each of the Children + foreach (Control element in Children) { - control.Measure(availableSize); - - var dock = control.GetValue(DockProperty); - if (IsHorizontal(dock)) - { - requiredHorizontalLength += control.DesiredSize.Width; - } - else + // Get the child's desired size + Size remainingSize = new Size( + Math.Max(0.0, constraint.Width - usedWidth), + Math.Max(0.0, constraint.Height - usedHeight)); + element.Measure(remainingSize); + Size desiredSize = element.DesiredSize; + + // Decrease the remaining space for the rest of the children + switch (GetDock(element)) { - requiredVerticalLength += control.DesiredSize.Height; + case Dock.Left: + case Dock.Right: + maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height); + usedWidth += desiredSize.Width; + break; + case Dock.Top: + case Dock.Bottom: + maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width); + usedHeight += desiredSize.Height; + break; } } - return new Size(requiredHorizontalLength, requiredVerticalLength); - } - - private static bool IsHorizontal(Dock dock) - { - return dock == Dock.Left || dock == Dock.Right; - } - - protected override Size ArrangeOverride(Size finalSize) - { - if (!LastChildFill) - { - return ArrangeAllChildren(finalSize); - } - else - { - return ArrangeChildrenAndFillLastChild(finalSize); - } - } - - private Size ArrangeChildrenAndFillLastChild(Size finalSize) - { - var docker = new DockingArranger(); - var requiredSize = docker.ArrangeAndGetUsedSize(finalSize, Children.WithoutLast()); - ArrangeToFill(Children.Last(), finalSize, docker.UsedMargin); - return requiredSize; - } - - private Size ArrangeAllChildren(Size finalSize) - { - return new DockingArranger().ArrangeAndGetUsedSize(finalSize, Children); + maximumWidth = Math.Max(maximumWidth, usedWidth); + maximumHeight = Math.Max(maximumHeight, usedHeight); + return new Size(maximumWidth, maximumHeight); } - private static void ArrangeToFill(ILayoutable layoutable, Size containerSize, Margin margin) + /// + protected override Size ArrangeOverride(Size arrangeSize) { - var containerRect = new Rect(new Point(0, 0), containerSize); - var marginsCutout = margin.AsThickness(); - var withoutMargins = containerRect.Deflate(marginsCutout); - - layoutable.Arrange(withoutMargins); - } + double left = 0.0; + double top = 0.0; + double right = 0.0; + double bottom = 0.0; - private class DockingArranger - { - public Margin UsedMargin { get; private set; } + // Arrange each of the Children + var children = Children; + int dockedCount = children.Count - (LastChildFill ? 1 : 0); + int index = 0; - public Size ArrangeAndGetUsedSize(Size availableSize, IEnumerable children) + foreach (Control element in children) { - var leftArranger = new LeftDocker(availableSize); - var rightArranger = new RightDocker(availableSize); - var topArranger = new LeftDocker(availableSize.Swap()); - var bottomArranger = new RightDocker(availableSize.Swap()); - - UsedMargin = new Margin(); - - foreach (var control in children) + // Determine the remaining space left to arrange the element + Rect remainingRect = new Rect( + left, + top, + Math.Max(0.0, arrangeSize.Width - left - right), + Math.Max(0.0, arrangeSize.Height - top - bottom)); + + // Trim the remaining Rect to the docked size of the element + // (unless the element should fill the remaining space because + // of LastChildFill) + if (index < dockedCount) { - Rect dockedRect; - var dock = control.GetValue(DockProperty); - switch (dock) + Size desiredSize = element.DesiredSize; + switch (GetDock(element)) { case Dock.Left: - dockedRect = leftArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments()); + left += desiredSize.Width; + remainingRect = remainingRect.WithWidth(desiredSize.Width); break; - case Dock.Top: - UsedMargin.Swap(); - dockedRect = topArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap(); - UsedMargin.Swap(); + top += desiredSize.Height; + remainingRect = remainingRect.WithHeight(desiredSize.Height); break; - case Dock.Right: - dockedRect = rightArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments()); + right += desiredSize.Width; + remainingRect = new Rect( + Math.Max(0.0, arrangeSize.Width - right), + remainingRect.Y, + desiredSize.Width, + remainingRect.Height); break; - case Dock.Bottom: - UsedMargin.Swap(); - dockedRect = bottomArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap(); - UsedMargin.Swap(); + bottom += desiredSize.Height; + remainingRect = new Rect( + remainingRect.X, + Math.Max(0.0, arrangeSize.Height - bottom), + remainingRect.Width, + desiredSize.Height); break; - - default: - throw new InvalidOperationException($"Invalid dock value {dock}"); } - - control.Arrange(dockedRect); } - return availableSize; - } - } - - private class LeftDocker : Docker - { - public LeftDocker(Size availableSize) : base(availableSize) - { - } - - public override Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments) - { - var marginsCutout = margin.AsThickness(); - var availableRect = OriginalRect.Deflate(marginsCutout); - var alignedRect = AlignToLeft(availableRect, childSize, alignments.Vertical); - - AccumulatedOffset += childSize.Width; - margin.Horizontal = margin.Horizontal.Offset(childSize.Width, 0); - - return alignedRect; - } - - private static Rect AlignToLeft(Rect availableRect, Size childSize, Alignment verticalAlignment) - { - return availableRect.AlignChild(childSize, Alignment.Start, verticalAlignment); - } - } - - private class RightDocker : Docker - { - public RightDocker(Size availableSize) : base(availableSize) - { - } - - public override Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments) - { - var marginsCutout = margin.AsThickness(); - var withoutMargins = OriginalRect.Deflate(marginsCutout); - var finalRect = withoutMargins.AlignChild(childSize, Alignment.End, alignments.Vertical); - - AccumulatedOffset += childSize.Width; - margin.Horizontal = margin.Horizontal.Offset(0, childSize.Width); - - return finalRect; - } - } - - private abstract class Docker - { - protected Docker(Size availableSize) - { - OriginalRect = new Rect(new Point(0, 0), availableSize); + element.Arrange(remainingRect); + index++; } - protected double AccumulatedOffset { get; set; } - - protected Rect OriginalRect { get; } - - public abstract Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments); - } - } - - public class Margin - { - public Segment Horizontal { get; set; } - public Segment Vertical { get; set; } - } - - public enum Alignment - { - Stretch, Start, Middle, End, - } - - public static class SegmentMixin - { - public static Segment AlignToStart(this Segment container, double length) - { - return new Segment(container.Start, container.Start + length); - } - - public static Segment AlignToEnd(this Segment container, double length) - { - return new Segment(container.End - length, container.End); - } - - public static Segment AlignToMiddle(this Segment container, double length) - { - var start = container.Start + (container.Length - length) / 2; - return new Segment(start, start + length); - } - } - - public struct Alignments - { - public Alignments(Alignment horizontal, Alignment vertical) - { - Horizontal = horizontal; - Vertical = vertical; - } - - public Alignment Horizontal { get; } - - public Alignment Vertical { get; } - } - - public static class CoordinateMixin - { - private static Point Swap(this Point p) - { - return new Point(p.Y, p.X); - } - - public static Size Swap(this Size s) - { - return new Size(s.Height, s.Width); - } - - public static Rect Swap(this Rect r) - { - return new Rect(r.Position.Swap(), r.Size.Swap()); - } - - public static Segment Offset(this Segment l, double startOffset, double endOffset) - { - return new Segment(l.Start + startOffset, l.End + endOffset); - } - - public static void Swap(this Margin m) - { - var v = m.Vertical; - m.Vertical = m.Horizontal; - m.Horizontal = v; - } - - public static Thickness AsThickness(this Margin margin) - { - return new Thickness(margin.Horizontal.Start, margin.Vertical.Start, margin.Horizontal.End, margin.Vertical.End); - } - - private static Alignment AsAlignment(this HorizontalAlignment horz) - { - switch (horz) - { - case HorizontalAlignment.Stretch: - return Alignment.Stretch; - case HorizontalAlignment.Left: - return Alignment.Start; - case HorizontalAlignment.Center: - return Alignment.Middle; - case HorizontalAlignment.Right: - return Alignment.End; - default: - throw new ArgumentOutOfRangeException(nameof(horz), horz, null); - } - } - - private static Alignment AsAlignment(this VerticalAlignment vert) - { - switch (vert) - { - case VerticalAlignment.Stretch: - return Alignment.Stretch; - case VerticalAlignment.Top: - return Alignment.Start; - case VerticalAlignment.Center: - return Alignment.Middle; - case VerticalAlignment.Bottom: - return Alignment.End; - default: - throw new ArgumentOutOfRangeException(nameof(vert), vert, null); - } - } - - public static Alignments GetAlignments(this ILayoutable layoutable) - { - return new Alignments(layoutable.HorizontalAlignment.AsAlignment(), layoutable.VerticalAlignment.AsAlignment()); - } - - public static Alignments Swap(this Alignments alignments) - { - return new Alignments(alignments.Vertical, alignments.Horizontal); - } - } - - public enum Dock - { - Left = 0, - Bottom, - Right, - Top - } - - public static class RectMixin - { - public static Rect AlignChild(this Rect container, Size childSize, Alignment horizontalAlignment, Alignment verticalAlignment) - { - var horzSegment = container.GetHorizontalCoordinates(); - var vertSegment = container.GetVerticalCoordinates(); - - var horzResult = GetAlignedSegment(childSize.Width, horizontalAlignment, horzSegment); - var vertResult = GetAlignedSegment(childSize.Height, verticalAlignment, vertSegment); - - return FromSegments(horzResult, vertResult); - } - - private static Rect FromSegments(Segment horzSegment, Segment vertSegment) - { - return new Rect(horzSegment.Start, vertSegment.Start, horzSegment.Length, vertSegment.Length); - } - - private static Segment GetAlignedSegment(double width, Alignment alignment, Segment horzSegment) - { - switch (alignment) - { - case Alignment.Start: - return horzSegment.AlignToStart(width); - - case Alignment.Middle: - return horzSegment.AlignToMiddle(width); - - case Alignment.End: - return horzSegment.AlignToEnd(width); - - default: - return new Segment(horzSegment.Start, horzSegment.End); - } - } - - private static Segment GetHorizontalCoordinates(this Rect rect) - { - return new Segment(rect.X, rect.Right); - } - - private static Segment GetVerticalCoordinates(this Rect rect) - { - return new Segment(rect.Y, rect.Bottom); - } - } - - public struct Segment - { - public Segment(double start, double end) - { - Start = start; - End = end; - } - - public double Start { get; } - public double End { get; } - - public double Length => End - Start; - - public override string ToString() - { - return $"Start: {Start}, End: {End}"; - } - } - - public static class EnumerableMixin - { - private static IEnumerable Shrink(this IEnumerable source, int left, int right) - { - int i = 0; - var buffer = new Queue(right + 1); - - foreach (T x in source) - { - if (i >= left) // Read past left many elements at the start - { - buffer.Enqueue(x); - if (buffer.Count > right) // Build a buffer to drop right many elements at the end - yield return buffer.Dequeue(); - } - else i++; - } - } - public static IEnumerable WithoutLast(this IEnumerable source, int n = 1) - { - return source.Shrink(0, n); - } - public static IEnumerable WithoutFirst(this IEnumerable source, int n = 1) - { - return source.Shrink(n, 0); + return arrangeSize; } } } \ No newline at end of file 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 +