Browse Source

Merge branch 'devtools'

pull/403/head
Steven Kirk 10 years ago
parent
commit
369313da00
  1. 12
      samples/ControlCatalog/App.paml.cs
  2. 9
      samples/ControlCatalog/ControlCatalog.csproj
  3. 4
      samples/ControlCatalog/packages.config
  4. 17
      src/Perspex.Base/PriorityValue.cs
  5. 27
      src/Perspex.Base/Threading/DispatcherTimer.cs
  6. 13
      src/Perspex.Controls/Generators/ItemContainerEventArgs.cs
  7. 19
      src/Perspex.Controls/Generators/TreeContainerIndex.cs
  8. 2
      src/Perspex.Controls/Primitives/HeaderedItemsControl.cs
  9. 57
      src/Perspex.Controls/TreeView.cs
  10. 22
      src/Perspex.Controls/TreeViewItem.cs
  11. 120
      src/Perspex.Diagnostics/DevTools.cs
  12. 18
      src/Perspex.Diagnostics/DevTools.paml
  13. 92
      src/Perspex.Diagnostics/DevTools.paml.cs
  14. 27
      src/Perspex.Diagnostics/Perspex.Diagnostics.csproj
  15. 2
      src/Perspex.Diagnostics/ViewLocator.cs
  16. 69
      src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs
  17. 8
      src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs
  18. 34
      src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs
  19. 20
      src/Perspex.Diagnostics/ViewModels/TreeNode.cs
  20. 102
      src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs
  21. 10
      src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs
  22. 34
      src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs
  23. 99
      src/Perspex.Diagnostics/Views/LogicalTreeView.cs
  24. 43
      src/Perspex.Diagnostics/Views/TreePage.cs
  25. 70
      src/Perspex.Diagnostics/Views/TreePage.paml.cs
  26. 24
      src/Perspex.Diagnostics/Views/TreePageView.paml
  27. 101
      src/Perspex.Diagnostics/Views/VisualTreeView.cs

12
samples/ControlCatalog/App.paml.cs

@ -5,6 +5,7 @@ using Perspex.Controls;
using Perspex.Diagnostics; using Perspex.Diagnostics;
using Perspex.Markup.Xaml; using Perspex.Markup.Xaml;
using Perspex.Themes.Default; using Perspex.Themes.Default;
using Serilog;
namespace ControlCatalog namespace ControlCatalog
{ {
@ -14,6 +15,7 @@ namespace ControlCatalog
{ {
RegisterServices(); RegisterServices();
InitializeSubsystems(GetPlatformId()); InitializeSubsystems(GetPlatformId());
InitializeLogging();
Styles = new DefaultTheme(); Styles = new DefaultTheme();
InitializeComponent(); InitializeComponent();
} }
@ -38,6 +40,16 @@ namespace ControlCatalog
PerspexXamlLoader.Load(this); PerspexXamlLoader.Load(this);
} }
private void InitializeLogging()
{
#if DEBUG
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Error()
.WriteTo.Trace(outputTemplate: "{Message}")
.CreateLogger();
#endif
}
private int GetPlatformId() private int GetPlatformId()
{ {
var args = Environment.GetCommandLineArgs(); var args = Environment.GetCommandLineArgs();

9
samples/ControlCatalog/ControlCatalog.csproj

@ -36,6 +36,14 @@
<StartupObject /> <StartupObject />
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
@ -93,6 +101,7 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="Pages\DropDownPage.paml" /> <EmbeddedResource Include="Pages\DropDownPage.paml" />
<None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Gtk\Perspex.Cairo\Perspex.Cairo.csproj"> <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> /// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority) 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; var old = _value;
if (_validate != null && value != PerspexProperty.UnsetValue) if (_validate != null && castValue != PerspexProperty.UnsetValue)
{ {
value = _validate(value); castValue = _validate(castValue);
} }
ValuePriority = priority; ValuePriority = priority;
_value = value; _value = castValue;
_changed.OnNext(Tuple.Create(old, _value)); _changed.OnNext(Tuple.Create(old, _value));
} }
else if (_logger != null) else if (_logger != null)
{ {
_logger.Error( _logger.Error(
"Binding produced invalid value for {$Type} {$Property}: {$Value}", "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_valueType,
_name, _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()); 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> /// <summary>
/// Starts the timer. /// Starts the timer.
/// </summary> /// </summary>

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

@ -12,6 +12,19 @@ namespace Perspex.Controls.Generators
/// </summary> /// </summary>
public class ItemContainerEventArgs : EventArgs 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> /// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class. /// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary> /// </summary>

19
src/Perspex.Controls/Generators/TreeContainerIndex.cs

@ -1,6 +1,7 @@
// Copyright (c) The Perspex Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Perspex.Controls.Generators 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<object, IControl> _itemToContainer = new Dictionary<object, IControl>();
private readonly Dictionary<IControl, object> _containerToItem = new Dictionary<IControl, object>(); 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> /// <summary>
/// Gets the currently materialized containers. /// Gets the currently materialized containers.
/// </summary> /// </summary>
@ -33,6 +44,10 @@ namespace Perspex.Controls.Generators
{ {
_itemToContainer.Add(item, container); _itemToContainer.Add(item, container);
_containerToItem.Add(container, item); _containerToItem.Add(container, item);
Materialized?.Invoke(
this,
new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
} }
/// <summary> /// <summary>
@ -44,6 +59,10 @@ namespace Perspex.Controls.Generators
var item = _containerToItem[container]; var item = _containerToItem[container];
_containerToItem.Remove(container); _containerToItem.Remove(container);
_itemToContainer.Remove(item); _itemToContainer.Remove(item);
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
} }
/// <summary> /// <summary>

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

@ -51,8 +51,8 @@ namespace Perspex.Controls.Primitives
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e) protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{ {
base.OnTemplateApplied(e);
HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter"); 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.Input;
using Perspex.Interactivity; using Perspex.Interactivity;
using Perspex.Styling; using Perspex.Styling;
using Perspex.Threading;
using Perspex.VisualTree; using Perspex.VisualTree;
namespace Perspex.Controls namespace Perspex.Controls
@ -17,6 +18,14 @@ namespace Perspex.Controls
/// </summary> /// </summary>
public class TreeView : ItemsControl 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> /// <summary>
/// Defines the <see cref="SelectedItem"/> property. /// Defines the <see cref="SelectedItem"/> property.
/// </summary> /// </summary>
@ -41,6 +50,15 @@ namespace Perspex.Controls
public new ITreeItemContainerGenerator ItemContainerGenerator => public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.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> /// <summary>
/// Gets or sets the selected item. /// Gets or sets the selected item.
/// </summary> /// </summary>
@ -65,6 +83,11 @@ namespace Perspex.Controls
{ {
var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem); var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
MarkContainerSelected(container, true); MarkContainerSelected(container, true);
if (AutoScrollToSelectedItem && container != null)
{
container.BringIntoView();
}
} }
} }
} }
@ -72,12 +95,14 @@ namespace Perspex.Controls
/// <inheritdoc/> /// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
return new TreeItemContainerGenerator<TreeViewItem>( var result = new TreeItemContainerGenerator<TreeViewItem>(
this, this,
TreeViewItem.HeaderProperty, TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty, TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty, TreeViewItem.IsExpandedProperty,
new TreeContainerIndex()); new TreeContainerIndex());
result.Index.Materialized += ContainerMaterialized;
return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -190,6 +215,36 @@ namespace Perspex.Controls
return null; 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> /// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>. /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
/// </summary> /// </summary>

22
src/Perspex.Controls/TreeViewItem.cs

@ -73,16 +73,12 @@ namespace Perspex.Controls
/// <inheritdoc/> /// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() protected override IItemContainerGenerator CreateItemContainerGenerator()
{ {
var result = new TreeItemContainerGenerator<TreeViewItem>( return new TreeItemContainerGenerator<TreeViewItem>(
this, this,
TreeViewItem.HeaderProperty, TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty, TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty, TreeViewItem.IsExpandedProperty,
_treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex()); _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
result.Materialized += ItemMaterialized;
return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -123,21 +119,5 @@ namespace Perspex.Controls
base.OnKeyDown(e); 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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- A reference to the entire .NET Framework is automatically included --> <!-- 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"> <ProjectReference Include="..\Perspex.Animation\Perspex.Animation.csproj">
<Project>{D211E587-D8BC-45B9-95A4-F297C8FA5200}</Project> <Project>{D211E587-D8BC-45B9-95A4-F297C8FA5200}</Project>
<Name>Perspex.Animation</Name> <Name>Perspex.Animation</Name>
@ -86,23 +94,24 @@
<Link>Properties\SharedAssemblyInfo.cs</Link> <Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile> </Compile>
<Compile Include="LogManager.cs" /> <Compile Include="LogManager.cs" />
<Compile Include="DevTools.cs" />
<Compile Include="Debug.cs" /> <Compile Include="Debug.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewLocator.cs" /> <Compile Include="ViewLocator.cs" />
<Compile Include="ViewModels\DevToolsViewModel.cs" /> <Compile Include="ViewModels\DevToolsViewModel.cs" />
<Compile Include="ViewModels\VisualTreeViewModel.cs" /> <Compile Include="ViewModels\TreePageViewModel.cs" />
<Compile Include="ViewModels\LogicalTreeViewModel.cs" />
<Compile Include="ViewModels\PropertyDetails.cs" /> <Compile Include="ViewModels\PropertyDetails.cs" />
<Compile Include="ViewModels\ControlDetailsViewModel.cs" /> <Compile Include="ViewModels\ControlDetailsViewModel.cs" />
<Compile Include="ViewModels\LogicalTreeNode.cs" /> <Compile Include="ViewModels\LogicalTreeNode.cs" />
<Compile Include="ViewModels\TreeNode.cs" /> <Compile Include="ViewModels\TreeNode.cs" />
<Compile Include="ViewModels\VisualTreeNode.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\ControlDetailsView.cs" />
<Compile Include="Views\GridRepeater.cs" /> <Compile Include="Views\GridRepeater.cs" />
<Compile Include="Views\VisualTreeView.cs" />
<Compile Include="Views\LogicalTreeView.cs" />
<Compile Include="Views\TreePage.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Splat"> <Reference Include="Splat">
@ -124,6 +133,12 @@
<ItemGroup> <ItemGroup>
<None Include="app.config" /> <None Include="app.config" />
<None Include="packages.config" /> <None Include="packages.config" />
<EmbeddedResource Include="DevTools.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Views\TreePageView.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> <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. <!-- 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 namespace Perspex.Diagnostics
{ {
internal class ViewLocator<TViewModel> : IDataTemplate public class ViewLocator<TViewModel> : IDataTemplate
{ {
public IControl Build(object data) public IControl Build(object data)
{ {

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

@ -11,54 +11,73 @@ namespace Perspex.Diagnostics.ViewModels
{ {
internal class DevToolsViewModel : ReactiveObject 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); switch (index)
VisualTree = new VisualTreeViewModel(_root); {
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
}
}); });
_focusedControl = KeyboardDevice.Instance _focusedControl = KeyboardDevice.Instance
.WhenAnyValue(x => x.FocusedElement) .WhenAnyValue(x => x.FocusedElement)
.Select(x => x?.GetType().Name)
.ToProperty(this, x => x.FocusedControl); .ToProperty(this, x => x.FocusedControl);
_pointerOverElement = this.WhenAnyValue(x => x.Root, x => x as TopLevel) _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty)
.Select(x => x?.GetObservable(TopLevel.PointerOverElementProperty) ?? Observable.Empty<IInputElement>()) .Select(x => x?.GetType().Name)
.Switch()
.ToProperty(this, x => x.PointerOverElement); .ToProperty(this, x => x.PointerOverElement);
} }
public Control Root public ReactiveObject Content
{ {
get { return _root; } get { return _content; }
set { this.RaiseAndSetIfChanged(ref _root, value); } private set { this.RaiseAndSetIfChanged(ref _content, value); }
} }
public LogicalTreeViewModel LogicalTree public int SelectedTab
{ {
get { return _logicalTree; } get { return _selectedTab; }
private set { this.RaiseAndSetIfChanged(ref _logicalTree, value); } set { this.RaiseAndSetIfChanged(ref _selectedTab, value); }
} }
public VisualTreeViewModel VisualTree public string FocusedControl => _focusedControl.Value;
{
get { return _visualTree; }
private set { this.RaiseAndSetIfChanged(ref _visualTree, 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 internal class LogicalTreeNode : TreeNode
{ {
public LogicalTreeNode(ILogical logical) public LogicalTreeNode(ILogical logical, TreeNode parent)
: base((Control)logical) : 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) public static LogicalTreeNode[] Create(object control)
{ {
var logical = control as ILogical; 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 internal class TreeNode : ReactiveObject
{ {
private string _classes; private string _classes;
private bool _isExpanded;
public TreeNode(Control control) public TreeNode(Control control, TreeNode parent)
{ {
Control = control; Control = control;
Parent = parent;
Type = control.GetType().Name; Type = control.GetType().Name;
var classesChanged = Observable.FromEventPattern< var classesChanged = Observable.FromEventPattern<
@ -52,13 +54,23 @@ namespace Perspex.Diagnostics.ViewModels
private set { this.RaiseAndSetIfChanged(ref _classes, value); } private set { this.RaiseAndSetIfChanged(ref _classes, value); }
} }
public string Type public Control Control
{ {
get; 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; get;
private set; 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 internal class VisualTreeNode : TreeNode
{ {
public VisualTreeNode(IVisual visual) public VisualTreeNode(IVisual visual, TreeNode parent)
: base((Control)visual) : base((Control)visual, parent)
{ {
var host = visual as IVisualTreeHost; var host = visual as IVisualTreeHost;
if (host?.Root == null) if (host?.Root == null)
{ {
Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x)); Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this));
} }
else else
{ {
Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root) }); Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
} }
if (Control != null) if (Control != null)
@ -34,7 +34,7 @@ namespace Perspex.Diagnostics.ViewModels
public static VisualTreeNode[] Create(object control) public static VisualTreeNode[] Create(object control)
{ {
var visual = control as IVisual; 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;
}
}
}
Loading…
Cancel
Save