Browse Source

Merge remote-tracking branch 'upstream/master'

pull/398/head
Ivan Kochurkin 11 years ago
parent
commit
2072912281
  1. 15
      docs/build.md
  2. 12
      samples/ControlCatalog/App.paml.cs
  3. 9
      samples/ControlCatalog/ControlCatalog.csproj
  4. 4
      samples/ControlCatalog/packages.config
  5. 17
      src/Perspex.Base/PriorityValue.cs
  6. 27
      src/Perspex.Base/Threading/DispatcherTimer.cs
  7. 513
      src/Perspex.Controls/DockPanel.cs
  8. 13
      src/Perspex.Controls/Generators/ItemContainerEventArgs.cs
  9. 19
      src/Perspex.Controls/Generators/TreeContainerIndex.cs
  10. 2
      src/Perspex.Controls/Primitives/HeaderedItemsControl.cs
  11. 57
      src/Perspex.Controls/TreeView.cs
  12. 22
      src/Perspex.Controls/TreeViewItem.cs
  13. 120
      src/Perspex.Diagnostics/DevTools.cs
  14. 18
      src/Perspex.Diagnostics/DevTools.paml
  15. 92
      src/Perspex.Diagnostics/DevTools.paml.cs
  16. 27
      src/Perspex.Diagnostics/Perspex.Diagnostics.csproj
  17. 2
      src/Perspex.Diagnostics/ViewLocator.cs
  18. 69
      src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs
  19. 8
      src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs
  20. 34
      src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs
  21. 20
      src/Perspex.Diagnostics/ViewModels/TreeNode.cs
  22. 102
      src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs
  23. 10
      src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs
  24. 34
      src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs
  25. 99
      src/Perspex.Diagnostics/Views/LogicalTreeView.cs
  26. 43
      src/Perspex.Diagnostics/Views/TreePage.cs
  27. 70
      src/Perspex.Diagnostics/Views/TreePage.paml.cs
  28. 24
      src/Perspex.Diagnostics/Views/TreePageView.paml
  29. 101
      src/Perspex.Diagnostics/Views/VisualTreeView.cs
  30. 40
      src/Perspex.SceneGraph/Rect.cs
  31. 16
      src/Skia/Perspex.Skia/DrawingContextImpl.cs
  32. 35
      src/Skia/Perspex.Skia/MethodTable.cs
  33. 59
      src/Skia/Perspex.Skia/PerspexHandleHolder.cs
  34. 40
      src/Skia/Perspex.Skia/StreamGeometryImpl.cs
  35. 10
      src/Skia/getnatives.sh
  36. 62
      tests/Perspex.Controls.UnitTests/DockPanelTests.cs
  37. 1
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  38. 12
      tests/Perspex.RenderTests/Shapes/PathTests.cs

15
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).

12
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();

9
samples/ControlCatalog/ControlCatalog.csproj

@ -36,6 +36,14 @@
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -93,6 +101,7 @@
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\DropDownPage.paml" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Gtk\Perspex.Cairo\Perspex.Cairo.csproj">

4
samples/ControlCatalog/packages.config

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Serilog" version="1.5.9" targetFramework="net46" />
</packages>

17
src/Perspex.Base/PriorityValue.cs

@ -224,26 +224,29 @@ namespace Perspex
/// <param name="priority">The priority level that the value came from.</param>
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());
}
}

27
src/Perspex.Base/Threading/DispatcherTimer.cs

@ -146,6 +146,33 @@ namespace Perspex.Threading
return Disposable.Create(() => timer.Stop());
}
/// <summary>
/// Runs a method once, after the specified interval.
/// </summary>
/// <param name="action">
/// The method to call after the interval has elapsed.
/// </param>
/// <param name="interval">The interval after which to call the method.</param>
/// <param name="priority">The priority to use.</param>
/// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
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());
}
/// <summary>
/// Starts the timer.
/// </summary>

513
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
/// <summary>
/// Defines the available docking modes for a control in a <see cref="DockPanel"/>.
/// </summary>
public enum Dock
{
public static readonly PerspexProperty<Dock> DockProperty = PerspexProperty.RegisterAttached<DockPanel, Control, Dock>("Dock");
Left = 0,
Bottom,
Right,
Top
}
/// <summary>
/// A panel which arranges its children at the top, bottom, left, right or center.
/// </summary>
public class DockPanel : Panel
{
/// <summary>
/// Defines the Dock attached property.
/// </summary>
public static readonly PerspexProperty<Dock> DockProperty =
PerspexProperty.RegisterAttached<DockPanel, Control, Dock>("Dock");
/// <summary>
/// Defines the <see cref="LastChildFill"/> property.
/// </summary>
public static readonly PerspexProperty<bool> LastChildFillProperty =
PerspexProperty.Register<DockPanel, bool>(
nameof(LastChildFillProperty),
defaultValue: true);
/// <summary>
/// Initializes static members of the <see cref="DockPanel"/> class.
/// </summary>
static DockPanel()
{
AffectsArrange(DockProperty);
}
// ReSharper disable once UnusedMember.Global
public static Dock GetDock(PerspexObject perspexObject)
/// <summary>
/// Gets the value of the Dock attached property on the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The Dock attached property.</returns>
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)
/// <summary>
/// Sets the value of the Dock attached property on the specified control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The value of the Dock property.</param>
public static void SetDock(Control control, Dock value)
{
element.SetValue(DockProperty, dock);
control.SetValue(DockProperty, value);
}
public static readonly PerspexProperty<bool> LastChildFillProperty = PerspexProperty.Register<DockPanel, bool>(nameof(LastChildFillProperty), defaultValue: true);
/// <summary>
/// Gets or sets a value which indicates whether the last child of the
/// <see cref="DockPanel"/> fills the remaining space in the panel.
/// </summary>
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<IControl> children)
/// <inheritdoc/>
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)
/// <inheritdoc/>
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<IControl> 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<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
{
int i = 0;
var buffer = new Queue<T>(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<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
{
return source.Shrink(0, n);
}
public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
{
return source.Shrink(n, 0);
return arrangeSize;
}
}
}

13
src/Perspex.Controls/Generators/ItemContainerEventArgs.cs

@ -12,6 +12,19 @@ namespace Perspex.Controls.Generators
/// </summary>
public class ItemContainerEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="container">The container.</param>
public ItemContainerEventArgs(
int startingIndex,
ItemContainer container)
{
StartingIndex = startingIndex;
Containers = new[] { container };
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>

19
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<object, IControl> _itemToContainer = new Dictionary<object, IControl>();
private readonly Dictionary<IControl, object> _containerToItem = new Dictionary<IControl, object>();
/// <summary>
/// Signalled whenever new containers are materialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs> Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Gets the currently materialized containers.
/// </summary>
@ -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)));
}
/// <summary>
@ -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)));
}
/// <summary>

2
src/Perspex.Controls/Primitives/HeaderedItemsControl.cs

@ -51,8 +51,8 @@ namespace Perspex.Controls.Primitives
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
base.OnTemplateApplied(e);
}
}
}

57
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
/// </summary>
public class TreeView : ItemsControl
{
/// <summary>
/// Defines the <see cref="AutoScrollToSelectedItem"/> property.
/// </summary>
public static readonly PerspexProperty<bool> AutoScrollToSelectedItemProperty =
PerspexProperty.Register<TreeView, bool>(
nameof(AutoScrollToSelectedItem),
defaultValue: true);
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
/// </summary>
@ -41,6 +50,15 @@ namespace Perspex.Controls
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <summary>
/// Gets or sets a value indicating whether to automatically scroll to newly selected items.
/// </summary>
public bool AutoScrollToSelectedItem
{
get { return GetValue(AutoScrollToSelectedItemProperty); }
set { SetValue(AutoScrollToSelectedItemProperty, value); }
}
/// <summary>
/// Gets or sets the selected item.
/// </summary>
@ -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
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new TreeItemContainerGenerator<TreeViewItem>(
var result = new TreeItemContainerGenerator<TreeViewItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
new TreeContainerIndex());
result.Index.Materialized += ContainerMaterialized;
return result;
}
/// <inheritdoc/>
@ -190,6 +215,36 @@ namespace Perspex.Controls
return null;
}
/// <summary>
/// Called when a new item container is materialized, to set its selected state.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
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;
}
}
}
}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary>

22
src/Perspex.Controls/TreeViewItem.cs

@ -73,16 +73,12 @@ namespace Perspex.Controls
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
var result = new TreeItemContainerGenerator<TreeViewItem>(
return new TreeItemContainerGenerator<TreeViewItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
_treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
result.Materialized += ItemMaterialized;
return result;
}
/// <inheritdoc/>
@ -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;
}
}
}
}
}
}

120
src/Perspex.Diagnostics/DevTools.cs

@ -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<Control> RootProperty =
PerspexProperty.Register<DevTools, Control>("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<ReactiveObject>());
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)")
}
}
}
}
};
}
}
}

18
src/Perspex.Diagnostics/DevTools.paml

@ -0,0 +1,18 @@
<UserControl xmlns="https://github.com/perspex">
<Grid RowDefinitions="Auto,*,Auto">
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
</TabStrip>
<ContentControl Content="{Binding Content}" Grid.Row="1"/>
<StackPanel Gap="4" Orientation="Horizontal" Grid.Row="2">
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator/>
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
</Grid>
</UserControl>

92
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<Window, Window> s_open = new Dictionary<Window, Window>();
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<ReactiveObject>(),
}
};
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);
}
}
}
}

27
src/Perspex.Diagnostics/Perspex.Diagnostics.csproj

@ -40,6 +40,14 @@
</PropertyGroup>
<ItemGroup>
<!-- A reference to the entire .NET Framework is automatically included -->
<ProjectReference Include="..\Markup\Perspex.Markup.Xaml\Perspex.Markup.Xaml.csproj">
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
<Name>Perspex.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\Markup\Perspex.Markup\Perspex.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Perspex.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\Perspex.Animation\Perspex.Animation.csproj">
<Project>{D211E587-D8BC-45B9-95A4-F297C8FA5200}</Project>
<Name>Perspex.Animation</Name>
@ -86,23 +94,24 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="LogManager.cs" />
<Compile Include="DevTools.cs" />
<Compile Include="Debug.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewLocator.cs" />
<Compile Include="ViewModels\DevToolsViewModel.cs" />
<Compile Include="ViewModels\VisualTreeViewModel.cs" />
<Compile Include="ViewModels\LogicalTreeViewModel.cs" />
<Compile Include="ViewModels\TreePageViewModel.cs" />
<Compile Include="ViewModels\PropertyDetails.cs" />
<Compile Include="ViewModels\ControlDetailsViewModel.cs" />
<Compile Include="ViewModels\LogicalTreeNode.cs" />
<Compile Include="ViewModels\TreeNode.cs" />
<Compile Include="ViewModels\VisualTreeNode.cs" />
<Compile Include="Views\TreePage.paml.cs">
<DependentUpon>TreePageView.paml</DependentUpon>
</Compile>
<Compile Include="DevTools.paml.cs">
<DependentUpon>DevTools.paml</DependentUpon>
</Compile>
<Compile Include="Views\ControlDetailsView.cs" />
<Compile Include="Views\GridRepeater.cs" />
<Compile Include="Views\VisualTreeView.cs" />
<Compile Include="Views\LogicalTreeView.cs" />
<Compile Include="Views\TreePage.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Splat">
@ -124,6 +133,12 @@
<ItemGroup>
<None Include="app.config" />
<None Include="packages.config" />
<EmbeddedResource Include="DevTools.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Views\TreePageView.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

2
src/Perspex.Diagnostics/ViewLocator.cs

@ -7,7 +7,7 @@ using Perspex.Controls.Templates;
namespace Perspex.Diagnostics
{
internal class ViewLocator<TViewModel> : IDataTemplate
public class ViewLocator<TViewModel> : IDataTemplate
{
public IControl Build(object data)
{

69
src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -11,54 +11,73 @@ namespace Perspex.Diagnostics.ViewModels
{
internal class DevToolsViewModel : ReactiveObject
{
private Control _root;
private IControl _root;
private LogicalTreeViewModel _logicalTree;
private ReactiveObject _content;
private VisualTreeViewModel _visualTree;
private int _selectedTab;
private readonly ObservableAsPropertyHelper<IInputElement> _focusedControl;
private TreePageViewModel _logicalTree;
private readonly ObservableAsPropertyHelper<IInputElement> _pointerOverElement;
private TreePageViewModel _visualTree;
public DevToolsViewModel()
private readonly ObservableAsPropertyHelper<string> _focusedControl;
private readonly ObservableAsPropertyHelper<string> _pointerOverElement;
public DevToolsViewModel(IControl root)
{
this.WhenAnyValue(x => x.Root).Subscribe(x =>
_root = root;
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
this.WhenAnyValue(x => x.SelectedTab).Subscribe(index =>
{
LogicalTree = new LogicalTreeViewModel(_root);
VisualTree = new VisualTreeViewModel(_root);
switch (index)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
}
});
_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<IInputElement>())
.Switch()
_pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty)
.Select(x => x?.GetType().Name)
.ToProperty(this, x => x.PointerOverElement);
}
public Control Root
public ReactiveObject Content
{
get { return _root; }
set { this.RaiseAndSetIfChanged(ref _root, value); }
get { return _content; }
private set { this.RaiseAndSetIfChanged(ref _content, value); }
}
public LogicalTreeViewModel LogicalTree
public int SelectedTab
{
get { return _logicalTree; }
private set { this.RaiseAndSetIfChanged(ref _logicalTree, value); }
get { return _selectedTab; }
set { this.RaiseAndSetIfChanged(ref _selectedTab, value); }
}
public VisualTreeViewModel VisualTree
{
get { return _visualTree; }
private set { this.RaiseAndSetIfChanged(ref _visualTree, value); }
}
public string FocusedControl => _focusedControl.Value;
public IInputElement FocusedControl => _focusedControl.Value;
public string PointerOverElement => _pointerOverElement.Value;
public IInputElement PointerOverElement => _pointerOverElement.Value;
public void SelectControl(IControl control)
{
var tree = Content as TreePageViewModel;
if (tree != null)
{
tree.SelectControl(control);
}
}
}
}

8
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;
}
}
}

34
src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs

@ -1,34 +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.Reactive.Linq;
using Perspex.Controls;
using ReactiveUI;
namespace Perspex.Diagnostics.ViewModels
{
internal class LogicalTreeViewModel : ReactiveObject
{
private LogicalTreeNode _selected;
private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
public LogicalTreeViewModel(Control root)
{
Nodes = LogicalTreeNode.Create(root);
_details = this.WhenAnyValue(x => x.SelectedNode)
.Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
.ToProperty(this, x => x.Details);
}
public LogicalTreeNode[] Nodes { get; }
public LogicalTreeNode SelectedNode
{
get { return _selected; }
set { this.RaiseAndSetIfChanged(ref _selected, value); }
}
public ControlDetailsViewModel Details => _details.Value;
}
}

20
src/Perspex.Diagnostics/ViewModels/TreeNode.cs

@ -13,10 +13,12 @@ namespace Perspex.Diagnostics.ViewModels
internal class TreeNode : ReactiveObject
{
private string _classes;
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<
@ -52,13 +54,23 @@ namespace Perspex.Diagnostics.ViewModels
private set { this.RaiseAndSetIfChanged(ref _classes, value); }
}
public string Type
public Control Control
{
get;
private set;
}
public Control Control
public bool IsExpanded
{
get { return _isExpanded; }
set { this.RaiseAndSetIfChanged(ref _isExpanded, value); }
}
public TreeNode Parent
{
get;
}
public string Type
{
get;
private set;

102
src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs

@ -0,0 +1,102 @@
// 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.Reactive.Linq;
using Perspex.Controls;
using Perspex.VisualTree;
using ReactiveUI;
namespace Perspex.Diagnostics.ViewModels
{
internal class TreePageViewModel : ReactiveObject
{
private TreeNode _selected;
private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
public TreePageViewModel(TreeNode[] nodes)
{
Nodes = nodes;
_details = this.WhenAnyValue(x => x.SelectedNode)
.Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
.ToProperty(this, x => x.Details);
}
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode
{
get { return _selected; }
set { this.RaiseAndSetIfChanged(ref _selected, value); }
}
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<IControl>();
}
}
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;
}
}
}

10
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<VisualTreeNode>(new[] { new VisualTreeNode(host.Root) });
Children = new ReactiveList<VisualTreeNode>(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;
}
}
}

34
src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs

@ -1,34 +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.Reactive.Linq;
using Perspex.Controls;
using ReactiveUI;
namespace Perspex.Diagnostics.ViewModels
{
internal class VisualTreeViewModel : ReactiveObject
{
private VisualTreeNode _selected;
private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
public VisualTreeViewModel(Control root)
{
Nodes = VisualTreeNode.Create(root);
_details = this.WhenAnyValue(x => x.SelectedNode)
.Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
.ToProperty(this, x => x.Details);
}
public VisualTreeNode[] Nodes { get; }
public VisualTreeNode SelectedNode
{
get { return _selected; }
set { this.RaiseAndSetIfChanged(ref _selected, value); }
}
public ControlDetailsViewModel Details => _details.Value;
}
}

99
src/Perspex.Diagnostics/Views/LogicalTreeView.cs

@ -1,99 +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 ReactiveUI;
namespace Perspex.Diagnostics.Views
{
using Controls = Controls.Controls;
internal class LogicalTreeView : TreePage
{
private static readonly PerspexProperty<LogicalTreeViewModel> ViewModelProperty =
PerspexProperty.Register<LogicalTreeView, LogicalTreeViewModel>("ViewModel");
public LogicalTreeView()
{
InitializeComponent();
this.GetObservable(DataContextProperty)
.Subscribe(x => ViewModel = (LogicalTreeViewModel)x);
}
public LogicalTreeViewModel 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<LogicalTreeNode>(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<LogicalTreeNode>()
.Subscribe(x => ViewModel.SelectedNode = x);
}
private Control GetHeader(LogicalTreeNode node)
{
var result = new StackPanel
{
Orientation = Orientation.Horizontal,
Gap = 8,
Children = new Controls
{
new TextBlock
{
Text = node.Type,
},
new TextBlock
{
[!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes),
}
}
};
result.PointerEnter += AddAdorner;
result.PointerLeave += RemoveAdorner;
return result;
}
}
}

43
src/Perspex.Diagnostics/Views/TreePage.cs

@ -1,43 +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 Perspex.Controls;
using Perspex.Controls.Primitives;
using Perspex.Controls.Shapes;
using Perspex.Diagnostics.ViewModels;
using Perspex.Input;
using Perspex.Media;
namespace Perspex.Diagnostics.Views
{
internal class TreePage : UserControl
{
private Control _adorner;
protected void AddAdorner(object sender, PointerEventArgs e)
{
var node = (TreeNode)((Control)sender).DataContext;
var layer = AdornerLayer.GetAdornerLayer(node.Control);
if (layer != null)
{
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
[AdornerLayer.AdornedElementProperty] = node.Control,
};
layer.Children.Add(_adorner);
}
}
protected void RemoveAdorner(object sender, PointerEventArgs e)
{
if (_adorner != null)
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
}
}
}
}

70
src/Perspex.Diagnostics/Views/TreePage.paml.cs

@ -0,0 +1,70 @@
using Perspex.Controls;
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Controls.Shapes;
using Perspex.Diagnostics.ViewModels;
using Perspex.Input;
using Perspex.Markup.Xaml;
using Perspex.Media;
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)
{
var node = (TreeNode)((Control)sender).DataContext;
var layer = AdornerLayer.GetAdornerLayer(node.Control);
if (layer != null)
{
_adorner = new Rectangle
{
Fill = new SolidColorBrush(0x80a0c5e8),
[AdornerLayer.AdornedElementProperty] = node.Control,
};
layer.Children.Add(_adorner);
}
}
protected void RemoveAdorner(object sender, PointerEventArgs e)
{
if (_adorner != null)
{
((Panel)_adorner.Parent).Children.Remove(_adorner);
_adorner = null;
}
}
private void InitializeComponent()
{
PerspexXamlLoader.Load(this);
_tree = this.FindControl<TreeView>("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;
}
}
}

24
src/Perspex.Diagnostics/Views/TreePageView.paml

@ -0,0 +1,24 @@
<UserControl xmlns="https://github.com/perspex"
xmlns:vm="clr-namespace:Perspex.Diagnostics.ViewModels;assembly=Perspex.Diagnostics">
<Grid ColumnDefinitions="*,4,3*">
<TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TreeNode"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Gap="8">
<TextBlock Text="{Binding Type}"/>
<TextBlock Text="{Binding Classes}"/>
</StackPanel>
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Width="4" Orientation="Vertical" Grid.Column="1"/>
<ContentControl Content="{Binding Details}" Grid.Column="2"/>
</Grid>
</UserControl>

101
src/Perspex.Diagnostics/Views/VisualTreeView.cs

@ -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<VisualTreeViewModel> ViewModelProperty =
PerspexProperty.Register<VisualTreeView, VisualTreeViewModel>("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<VisualTreeNode>(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<VisualTreeNode>()
.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;
}
}
}

40
src/Perspex.SceneGraph/Rect.cs

@ -402,6 +402,46 @@ namespace Perspex
return new Rect(Position + offset, Size);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified X position.
/// </summary>
/// <param name="x">The x position.</param>
/// <returns>The new <see cref="Rect"/>.</returns>
public Rect WithX(double x)
{
return new Rect(x, _y, _width, _height);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified Y position.
/// </summary>
/// <param name="y">The y position.</param>
/// <returns>The new <see cref="Rect"/>.</returns>
public Rect WithY(double y)
{
return new Rect(_x, y, _width, _height);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified width.
/// </summary>
/// <param name="width">The width.</param>
/// <returns>The new <see cref="Rect"/>.</returns>
public Rect WithWidth(double width)
{
return new Rect(_x, _y, width, _height);
}
/// <summary>
/// Returns a new <see cref="Rect"/> with the specified height.
/// </summary>
/// <param name="height">The height.</param>
/// <returns>The new <see cref="Rect"/>.</returns>
public Rect WithHeight(double height)
{
return new Rect(_x, _y, _width, height);
}
/// <summary>
/// Returns the string representation of the rectangle.
/// </summary>

16
src/Skia/Perspex.Skia/DrawingContextImpl.cs

@ -37,18 +37,13 @@ namespace Perspex.Skia
public void DrawGeometry(Brush brush, Pen pen, Geometry geometry)
{
var impl = ((StreamGeometryImpl) geometry.PlatformImpl);
var oldTransform = Transform;
if (!impl.Transform.IsIdentity)
Transform = impl.Transform*Transform;
var size = geometry.Bounds.Size;
using(var fill = brush!=null?CreateBrush(brush, size):null)
using (var stroke = pen?.Brush != null ? CreateBrush(pen, size) : null)
{
MethodTable.Instance.DrawGeometry(Handle, impl.Path.Handle, fill != null ? fill.Brush : null,
MethodTable.Instance.DrawGeometry(Handle, impl.EffectivePath, fill != null ? fill.Brush : null,
stroke != null ? stroke.Brush : null, impl.FillRule == FillRule.EvenOdd);
}
Transform = oldTransform;
}
unsafe NativeBrushContainer CreateBrush(Brush brush, Size targetSize)
@ -194,14 +189,7 @@ namespace Perspex.Skia
if(_currentTransform == value)
return;
_currentTransform = value;
_fmatrix[0] = (float)value.M11;
_fmatrix[1] = (float)value.M21;
_fmatrix[2] = (float)value.M31;
_fmatrix[3] = (float)value.M12;
_fmatrix[4] = (float)value.M22;
_fmatrix[5] = (float)value.M32;
MethodTable.Instance.SetTransform(Handle, _fmatrix);
MethodTable.Instance.SetTransform(Handle, value);
}
}
}

35
src/Skia/Perspex.Skia/MethodTable.cs

@ -47,9 +47,9 @@ namespace Perspex.Skia
public _PopClip PopClip;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void _SetTransform(IntPtr ctx, float[] matrix6);
public delegate void _SetTransform(IntPtr ctx, void* matrix6);
public _SetTransform SetTransform;
public _SetTransform SetTransformNative;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void _DrawLine(IntPtr ctx, void* brush, float x1, float y1, float x2, float y2);
@ -66,6 +66,11 @@ namespace Perspex.Skia
public _DisposePath DisposePath;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr _TransformPath(IntPtr path, void* matrix6);
public _TransformPath TransformPathNative;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void _DrawGeometry(IntPtr ctx, IntPtr path, void* fill, void* stroke, bool useEvenOdd);
@ -185,10 +190,34 @@ namespace Perspex.Skia
typeof (_RebuildFormattedText),
typeof (_DestroyFormattedText),
typeof (_DrawFormattedText),
typeof (_SetOption)
typeof (_SetOption),
typeof (_TransformPath)
};
void ConvertMatrix(Matrix value, float* target)
{
target[0] = (float)value.M11;
target[1] = (float)value.M21;
target[2] = (float)value.M31;
target[3] = (float)value.M12;
target[4] = (float)value.M22;
target[5] = (float)value.M32;
}
public unsafe IntPtr TransformPath(IntPtr path, Matrix matrix)
{
var tmp = stackalloc float[6];
ConvertMatrix(matrix, tmp);
return TransformPathNative(path, tmp);
}
public unsafe void SetTransform(IntPtr ctx, Matrix matrix)
{
var tmp = stackalloc float[6];
ConvertMatrix(matrix, tmp);
SetTransformNative(ctx, tmp);
}
protected MethodTable(IntPtr methodTable)
{

59
src/Skia/Perspex.Skia/PerspexHandleHolder.cs

@ -44,4 +44,63 @@ namespace Perspex.Skia
Dispose();
}
}
class RefCountable<T> : IDisposable where T : PerspexHandleHolder
{
class Shared
{
public readonly T Target;
private int _refCount = 1;
public Shared(T target)
{
Target = target;
}
public void AddRef() => _refCount++;
public void Release()
{
_refCount--;
if (_refCount <= 0)
Target.Dispose();
}
}
public bool IsDisposed => _shared == null;
private Shared _shared;
public void CheckDisposed()
{
if (IsDisposed)
throw new ObjectDisposedException(GetType().FullName);
}
public IntPtr Handle
{
get
{
CheckDisposed();
return _shared.Target.Handle;
}
}
public RefCountable(T handle)
{
_shared = new Shared(handle);
}
public RefCountable(RefCountable<T> other)
{
other._shared.Target.CheckDisposed();
other._shared.AddRef();
_shared = other._shared;
}
public RefCountable<T> Clone() => new RefCountable<T>(this);
public void Dispose()
{
_shared?.Release();
_shared = null;
}
}
}

40
src/Skia/Perspex.Skia/StreamGeometryImpl.cs

@ -41,7 +41,11 @@ namespace Perspex.Skia
class StreamGeometryImpl : IStreamGeometryImpl
{
public SkPath Path;
RefCountable<SkPath> _path;
RefCountable<SkPath> _transformedPath;
private Matrix _transform = Matrix.Identity;
public IntPtr EffectivePath => (_transformedPath ?? _path).Handle;
public Rect GetRenderBounds(double strokeThickness)
{
@ -51,11 +55,35 @@ namespace Perspex.Skia
public Rect Bounds { get; private set; }
public Matrix Transform { get; set; } = Matrix.Identity;
public Matrix Transform
{
get { return _transform; }
set
{
if(_transform == value)
return;
_transform = value;
ApplyTransform();
}
}
void ApplyTransform()
{
if(_path == null)
return;
if (_transformedPath != null)
{
_transformedPath.Dispose();
_transformedPath = null;
}
if (!_transform.IsIdentity)
_transformedPath =
new RefCountable<SkPath>(new SkPath(MethodTable.Instance.TransformPath(_path.Handle, Transform)));
}
public IStreamGeometryImpl Clone()
{
return new StreamGeometryImpl() {Path = Path, Transform = Transform, Bounds = Bounds};
return new StreamGeometryImpl {_path = _path?.Clone(), _transformedPath = _transformedPath?.Clone(), _transform = Transform, Bounds = Bounds};
}
public IStreamGeometryContextImpl Open()
@ -77,7 +105,11 @@ namespace Perspex.Skia
{
var arr = _elements.ToArray();
SkRect rc;
_geometryImpl.Path = new SkPath(MethodTable.Instance.CreatePath(arr, arr.Length, out rc));
_geometryImpl._path?.Dispose();
_geometryImpl._path =
new RefCountable<SkPath>(new SkPath(MethodTable.Instance.CreatePath(arr, arr.Length, out rc)));
_geometryImpl.ApplyTransform();
_geometryImpl.Bounds = rc.ToRect();
}

10
src/Skia/getnatives.sh

@ -1,7 +1,13 @@
#!/bin/sh
rm -rf native
rm -rf native native.zip
mkdir -p native
cd native
if which curl
then
curl `cat ../native.url` -o native.zip
else
wget `cat ../native.url` -O native.zip
unzip native.zip
fi
unzip native.zip
chmod -R +x .

62
tests/Perspex.Controls.UnitTests/DockPanelTests.cs

@ -0,0 +1,62 @@
// 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 Xunit;
namespace Perspex.Controls.UnitTests
{
public class DockPanelTests
{
[Fact]
public void Should_Dock_Controls_Horizontal_First()
{
var target = new DockPanel
{
Children = new Controls
{
new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top },
new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom },
new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left },
new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right },
new Border { },
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Rect(0, 0, 500, 500), target.Bounds);
Assert.Equal(new Rect(0, 0, 500, 50), target.Children[0].Bounds);
Assert.Equal(new Rect(0, 450, 500, 50), target.Children[1].Bounds);
Assert.Equal(new Rect(0, 50, 50, 400), target.Children[2].Bounds);
Assert.Equal(new Rect(450, 50, 50, 400), target.Children[3].Bounds);
Assert.Equal(new Rect(50, 50, 400, 400), target.Children[4].Bounds);
}
[Fact]
public void Should_Dock_Controls_Vertical_First()
{
var target = new DockPanel
{
Children = new Controls
{
new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left },
new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right },
new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top },
new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom },
new Border { },
}
};
target.Measure(Size.Infinity);
target.Arrange(new Rect(target.DesiredSize));
Assert.Equal(new Rect(0, 0, 600, 400), target.Bounds);
Assert.Equal(new Rect(0, 0, 50, 400), target.Children[0].Bounds);
Assert.Equal(new Rect(550, 0, 50, 400), target.Children[1].Bounds);
Assert.Equal(new Rect(50, 0, 500, 50), target.Children[2].Bounds);
Assert.Equal(new Rect(50, 350, 500, 50), target.Children[3].Bounds);
Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds);
}
}
}

1
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -82,6 +82,7 @@
</Choose>
<ItemGroup>
<Compile Include="ClassesTests.cs" />
<Compile Include="DockPanelTests.cs" />
<Compile Include="EnumerableExtensions.cs" />
<Compile Include="GridSplitterTests.cs" />
<Compile Include="GridTests.cs" />

12
tests/Perspex.RenderTests/Shapes/PathTests.cs

@ -46,11 +46,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
CompareImages();
}
#if PERSPEX_SKIA
[Fact(Skip = "FIXME")]
#else
[Fact]
#endif
public void Path_Tick_Scaled()
{
Decorator target = new Decorator
@ -73,11 +69,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
CompareImages();
}
#if PERSPEX_SKIA
[Fact(Skip = "FIXME")]
#else
[Fact]
#endif
public void Path_Tick_Scaled_Stroke_8px()
{
Decorator target = new Decorator
@ -100,11 +92,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
CompareImages();
}
#if PERSPEX_SKIA
[Fact(Skip = "FIXME")]
#else
[Fact]
#endif
public void Path_Expander_With_Border()
{
Decorator target = new Decorator

Loading…
Cancel
Save