diff --git a/src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf b/src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf new file mode 100644 index 0000000000..278ad8aa0a Binary files /dev/null and b/src/Avalonia.Diagnostics/Assets/Fonts/SourceSansPro-Regular.ttf differ diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 7d0686a33f..5f49a46839 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -1,8 +1,15 @@  netstandard2.0 + Avalonia + + %(Filename) + + + + @@ -14,7 +21,10 @@ - + + + + diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml deleted file mode 100644 index 1df0f3a097..0000000000 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - Hold Ctrl+Shift over a control to inspect. - - Focused: - - - Pointer Over: - - - - diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs deleted file mode 100644 index 037e80e372..0000000000 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) The Avalonia 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; -using System.Linq; -using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Diagnostics.ViewModels; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; -using Avalonia.Rendering; -using Avalonia.VisualTree; - -namespace Avalonia -{ - public static class DevToolsExtensions - { - public static void AttachDevTools(this TopLevel control) - { - Diagnostics.DevTools.Attach(control, new KeyGesture(Key.F12)); - } - - public static void AttachDevTools(this TopLevel control, KeyGesture gesture) - { - Diagnostics.DevTools.Attach(control, gesture); - } - - public static void OpenDevTools(this TopLevel control) - { - Diagnostics.DevTools.OpenDevTools(control); - } - } -} - -namespace Avalonia.Diagnostics -{ - public class DevTools : UserControl - { - private static readonly Dictionary s_open = new Dictionary(); - private static readonly HashSet s_visualTreeRoots = new HashSet(); - private readonly IDisposable _keySubscription; - - public DevTools(IControl root) - { - InitializeComponent(); - Root = root; - DataContext = new DevToolsViewModel(root); - - _keySubscription = InputManager.Instance.Process - .OfType() - .Subscribe(RawKeyDown); - } - - // HACK: needed for XAMLIL, will fix that later - public DevTools() - { - } - - public IControl Root { get; } - - public static IDisposable Attach(TopLevel control, KeyGesture gesture) - { - void PreviewKeyDown(object sender, KeyEventArgs e) - { - if (gesture.Matches(e)) - { - OpenDevTools(control); - } - } - - return control.AddHandler( - KeyDownEvent, - PreviewKeyDown, - RoutingStrategies.Tunnel); - } - - internal static void OpenDevTools(TopLevel control) - { - if (s_open.TryGetValue(control, out var devToolsWindow)) - { - devToolsWindow.Activate(); - } - else - { - var devTools = new DevTools(control); - - devToolsWindow = new Window - { - Width = 1024, - Height = 512, - Content = devTools, - DataTemplates = { new ViewLocator() }, - Title = "Avalonia DevTools" - }; - - devToolsWindow.Closed += devTools.DevToolsClosed; - s_open.Add(control, devToolsWindow); - MarkAsDevTool(devToolsWindow); - devToolsWindow.Show(); - } - } - - private void DevToolsClosed(object sender, EventArgs e) - { - var devToolsWindow = (Window)sender; - var devTools = (DevTools)devToolsWindow.Content; - s_open.Remove((TopLevel)devTools.Root); - RemoveDevTool(devToolsWindow); - _keySubscription.Dispose(); - devToolsWindow.Closed -= DevToolsClosed; - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private void RawKeyDown(RawKeyEventArgs e) - { - const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; - - if (e.Modifiers == modifiers) - { - var point = (Root.VisualRoot as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default(Point); - var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) - .FirstOrDefault(); - - if (control != null) - { - var vm = (DevToolsViewModel)DataContext; - vm.SelectControl((IControl)control); - } - } - } - - /// - /// Marks a visual as part of the DevTools, so it can be excluded from event tracking. - /// - /// The visual whose root is to be marked. - public static void MarkAsDevTool(IVisual visual) - { - s_visualTreeRoots.Add(visual.GetVisualRoot()); - } - - public static void RemoveDevTool(IVisual visual) - { - s_visualTreeRoots.Remove(visual.GetVisualRoot()); - } - - public static bool BelongsToDevTool(IVisual visual) - { - return s_visualTreeRoots.Contains(visual.GetVisualRoot()); - } - } -} diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs new file mode 100644 index 0000000000..8083d4bc58 --- /dev/null +++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; +using Avalonia.Diagnostics; +using Avalonia.Input; + +namespace Avalonia +{ + public static class DevToolsExtensions + { + public static void AttachDevTools(this TopLevel root) + { + DevTools.Attach(root, new KeyGesture(Key.F12)); + } + + public static void AttachDevTools(this TopLevel root, KeyGesture gesture) + { + DevTools.Attach(root, gesture); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs new file mode 100644 index 0000000000..37ba5155fd --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToBrushConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Diagnostics.Converters +{ + internal class BoolToBrushConverter : IValueConverter + { + public IBrush Brush { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return (bool)value ? Brush : Brushes.Transparent; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs new file mode 100644 index 0000000000..0464047273 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Diagnostics.Views; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Diagnostics +{ + public static class DevTools + { + private static readonly Dictionary s_open = new Dictionary(); + + public static IDisposable Attach(TopLevel root, KeyGesture gesture) + { + void PreviewKeyDown(object sender, KeyEventArgs e) + { + if (gesture.Matches(e)) + { + Open(root); + } + } + + return root.AddHandler( + InputElement.KeyDownEvent, + PreviewKeyDown, + RoutingStrategies.Tunnel); + } + + public static IDisposable Open(TopLevel root) + { + if (s_open.TryGetValue(root, out var window)) + { + window.Activate(); + } + else + { + window = new MainWindow + { + Width = 1024, + Height = 512, + Root = root, + }; + + window.Closed += DevToolsClosed; + s_open.Add(root, window); + window.Show(); + } + + return Disposable.Create(() => window?.Close()); + } + + private static void DevToolsClosed(object sender, EventArgs e) + { + var window = (MainWindow)sender; + s_open.Remove(window.Root); + window.Closed -= DevToolsClosed; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs new file mode 100644 index 0000000000..5927bd785e --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleContext.cs @@ -0,0 +1,36 @@ +#pragma warning disable IDE1006 // Naming Styles + +using Avalonia.Diagnostics.ViewModels; + +namespace Avalonia.Diagnostics.Models +{ + public class ConsoleContext + { + private readonly ConsoleViewModel _owner; + + internal ConsoleContext(ConsoleViewModel owner) => _owner = owner; + + public readonly string help = @"Welcome to Avalonia DevTools. Here you can execute arbitrary C# code using Roslyn scripting. + +The following variables are available: + +e: The control currently selected in the logical or visual tree view +root: The root of the visual tree + +The following commands are available: + +clear(): Clear the output history +"; + + public dynamic e { get; internal set; } + public dynamic root { get; internal set; } + + internal static object NoOutput { get; } = new object(); + + public object clear() + { + _owner.History.Clear(); + return NoOutput; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs new file mode 100644 index 0000000000..8fad299f60 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Models/ConsoleHistoryItem.cs @@ -0,0 +1,19 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Diagnostics.Models +{ + public class ConsoleHistoryItem + { + public ConsoleHistoryItem(string input, object output) + { + Input = input; + Output = output; + Foreground = output is Exception ? Brushes.Red : Brushes.Green; + } + + public string Input { get; } + public object Output { get; } + public IBrush Foreground { get; } + } +} diff --git a/src/Avalonia.Diagnostics/Models/EventChainLink.cs b/src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs similarity index 100% rename from src/Avalonia.Diagnostics/Models/EventChainLink.cs rename to src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs similarity index 70% rename from src/Avalonia.Diagnostics/ViewLocator.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs index a66703301d..c06fbec801 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs @@ -1,13 +1,11 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using System; using Avalonia.Controls; using Avalonia.Controls.Templates; +using Avalonia.Diagnostics.ViewModels; namespace Avalonia.Diagnostics { - internal class ViewLocator : IDataTemplate + internal class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; @@ -28,7 +26,7 @@ namespace Avalonia.Diagnostics public bool Match(object data) { - return data is TViewModel; + return data is ViewModelBase; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs new file mode 100644 index 0000000000..56d274111d --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs @@ -0,0 +1,120 @@ +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Collections; +using Avalonia.Data.Converters; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class AvaloniaPropertyViewModel : PropertyViewModel + { + private readonly AvaloniaObject _target; + private object _value; + private string _priority; + private TypeConverter _converter; + private string _group; + private DataGridCollectionView _bindingsView; + + public AvaloniaPropertyViewModel(AvaloniaObject o, AvaloniaProperty property) + { + _target = o; + Property = property; + + Name = property.IsAttached ? + $"[{property.OwnerType.Name}.{property.Name}]" : + property.Name; + + if (property.IsDirect) + { + _group = "Properties"; + Priority = "Direct"; + } + + Update(); + } + + public AvaloniaProperty Property { get; } + public override object Key => Property; + public override string Name { get; } + public bool IsAttached => Property.IsAttached; + + public string Priority + { + get => _priority; + private set => RaiseAndSetIfChanged(ref _priority, value); + } + + public override string Value + { + get + { + if (_value == null) + { + return "(null)"; + } + + return Converter?.CanConvertTo(typeof(string)) == true ? + Converter.ConvertToString(_value) : + _value.ToString(); + } + set + { + try + { + var convertedValue = Converter?.CanConvertFrom(typeof(string)) == true ? + Converter.ConvertFromString(value) : + DefaultValueConverter.Instance.ConvertBack(value, Property.PropertyType, null, CultureInfo.CurrentCulture); + _target.SetValue(Property, convertedValue); + } + catch { } + } + } + + public override string Group + { + get => _group; + } + + private TypeConverter Converter + { + get + { + if (_converter == null) + { + _converter = TypeDescriptor.GetConverter(_value.GetType()); + } + + return _converter; + } + } + + public override void Update() + { + if (Property.IsDirect) + { + RaiseAndSetIfChanged(ref _value, _target.GetValue(Property), nameof(Value)); + } + else + { + var val = _target.GetDiagnostic(Property); + + RaiseAndSetIfChanged(ref _value, val?.Value, nameof(Value)); + + if (val != null) + { + SetGroup(IsAttached ? "Attached Properties" : "Properties"); + Priority = val.Priority.ToString(); + } + else + { + SetGroup(Priority = "Unset"); + } + } + } + + private void SetGroup(string group) + { + RaiseAndSetIfChanged(ref _group, group, nameof(Group)); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs new file mode 100644 index 0000000000..3fe6c93d28 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs @@ -0,0 +1,82 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using Avalonia.Data.Converters; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class ClrPropertyViewModel : PropertyViewModel + { + private readonly object _target; + private object _value; + private TypeConverter _converter; + + public ClrPropertyViewModel(object o, PropertyInfo property) + { + _target = o; + Property = property; + + if (!property.DeclaringType.IsInterface) + { + Name = property.Name; + } + else + { + Name = property.DeclaringType.Name + '.' + property.Name; + } + + Update(); + } + + public PropertyInfo Property { get; } + public override object Key => Name; + public override string Name { get; } + public override string Group => "CLR Properties"; + + public override string Value + { + get + { + if (_value == null) + { + return "(null)"; + } + + return Converter?.CanConvertTo(typeof(string)) == true ? + Converter.ConvertToString(_value) : + _value.ToString(); + } + set + { + try + { + var convertedValue = Converter?.CanConvertFrom(typeof(string)) == true ? + Converter.ConvertFromString(value) : + DefaultValueConverter.Instance.ConvertBack(value, Property.PropertyType, null, CultureInfo.CurrentCulture); + Property.SetValue(_target, convertedValue); + } + catch { } + } + } + + private TypeConverter Converter + { + get + { + if (_converter == null) + { + _converter = TypeDescriptor.GetConverter(_value.GetType()); + } + + return _converter; + } + } + + public override void Update() + { + var val = Property.GetValue(_target); + RaiseAndSetIfChanged(ref _value, val, nameof(Value)); + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs new file mode 100644 index 0000000000..67adc255a2 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ConsoleViewModel.cs @@ -0,0 +1,112 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Collections; +using Avalonia.Diagnostics.Models; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class ConsoleViewModel : ViewModelBase + { + readonly ConsoleContext _context; + readonly Action _updateContext; + private int _historyIndex = -1; + private string _input; + private bool _isVisible; + private ScriptState _state; + + public ConsoleViewModel(Action updateContext) + { + _context = new ConsoleContext(this); + _updateContext = updateContext; + } + + public string Input + { + get => _input; + set => RaiseAndSetIfChanged(ref _input, value); + } + + public bool IsVisible + { + get => _isVisible; + set => RaiseAndSetIfChanged(ref _isVisible, value); + } + + public AvaloniaList History { get; } = new AvaloniaList(); + + public async Task Execute() + { + if (string.IsNullOrWhiteSpace(Input)) + { + return; + } + + try + { + var options = ScriptOptions.Default + .AddReferences(Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo))); + + _updateContext(_context); + + if (_state == null) + { + _state = await CSharpScript.RunAsync(Input, options: options, globals: _context); + } + else + { + _state = await _state.ContinueWithAsync(Input); + } + + if (_state.ReturnValue != ConsoleContext.NoOutput) + { + History.Add(new ConsoleHistoryItem(Input, _state.ReturnValue ?? "(null)")); + } + } + catch (Exception ex) + { + History.Add(new ConsoleHistoryItem(Input, ex)); + } + + Input = string.Empty; + _historyIndex = -1; + } + + public void HistoryUp() + { + if (History.Count > 0) + { + if (_historyIndex == -1) + { + _historyIndex = History.Count - 1; + } + else if (_historyIndex > 0) + { + --_historyIndex; + } + + Input = History[_historyIndex].Input; + } + } + + public void HistoryDown() + { + if (History.Count > 0 && _historyIndex >= 0) + { + if (_historyIndex == History.Count - 1) + { + _historyIndex = -1; + Input = string.Empty; + } + else + { + Input = History[++_historyIndex].Input; + } + } + } + + public void ToggleVisibility() => IsVisible = !IsVisible; + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs new file mode 100644 index 0000000000..e712241282 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Avalonia.Collections; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class ControlDetailsViewModel : ViewModelBase, IDisposable + { + private readonly IVisual _control; + private readonly IDictionary> _propertyIndex; + private AvaloniaPropertyViewModel _selectedProperty; + private string _propertyFilter; + + public ControlDetailsViewModel(IVisual control, string propertyFilter) + { + _control = control; + + var properties = GetAvaloniaProperties(control) + .Concat(GetClrProperties(control)) + .OrderBy(x => x, PropertyComparer.Instance) + .ThenBy(x => x.Name) + .ToList(); + + _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList()); + _propertyFilter = propertyFilter; + + var view = new DataGridCollectionView(properties); + view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); + view.Filter = FilterProperty; + PropertiesView = view; + + if (control is INotifyPropertyChanged inpc) + { + inpc.PropertyChanged += ControlPropertyChanged; + } + + if (control is AvaloniaObject ao) + { + ao.PropertyChanged += ControlPropertyChanged; + } + } + + public IEnumerable Classes + { + get; + private set; + } + + public DataGridCollectionView PropertiesView { get; } + + public string PropertyFilter + { + get => _propertyFilter; + set + { + if (RaiseAndSetIfChanged(ref _propertyFilter, value)) + { + PropertiesView.Refresh(); + } + } + } + + public AvaloniaPropertyViewModel SelectedProperty + { + get => _selectedProperty; + set => RaiseAndSetIfChanged(ref _selectedProperty, value); + } + + public void Dispose() + { + if (_control is INotifyPropertyChanged inpc) + { + inpc.PropertyChanged -= ControlPropertyChanged; + } + + if (_control is AvaloniaObject ao) + { + ao.PropertyChanged -= ControlPropertyChanged; + } + } + + private IEnumerable GetAvaloniaProperties(object o) + { + if (o is AvaloniaObject ao) + { + return AvaloniaPropertyRegistry.Instance.GetRegistered(ao) + .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType())) + .Select(x => new AvaloniaPropertyViewModel(ao, x)); + } + else + { + return Enumerable.Empty(); + } + } + + private IEnumerable GetClrProperties(object o) + { + foreach (var p in GetClrProperties(o, o.GetType())) + { + yield return p; + } + + foreach (var i in o.GetType().GetInterfaces()) + { + foreach (var p in GetClrProperties(o, i)) + { + yield return p; + } + } + } + + private IEnumerable GetClrProperties(object o, Type t) + { + return t.GetProperties() + .Where(x => x.GetIndexParameters().Length == 0) + .Select(x => new ClrPropertyViewModel(o, x)); + } + + private void ControlPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (_propertyIndex.TryGetValue(e.Property, out var properties)) + { + foreach (var property in properties) + { + property.Update(); + } + } + } + + private void ControlPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_propertyIndex.TryGetValue(e.PropertyName, out var properties)) + { + foreach (var property in properties) + { + property.Update(); + } + } + } + + private bool FilterProperty(object arg) + { + if (!string.IsNullOrWhiteSpace(PropertyFilter) && arg is PropertyViewModel property) + { + return property.Name.IndexOf(PropertyFilter, StringComparison.OrdinalIgnoreCase) != -1; + } + + return true; + } + + private class PropertyComparer : IComparer + { + public static PropertyComparer Instance { get; } = new PropertyComparer(); + + public int Compare(PropertyViewModel x, PropertyViewModel y) + { + var groupX = GroupIndex(x.Group); + var groupY = GroupIndex(y.Group); + + if (groupX != groupY) + { + return groupX - groupY; + } + else + { + return string.Compare(x.Name, y.Name); + } + } + + private int GroupIndex(string group) + { + switch (group) + { + case "Properties": return 0; + case "Attached Properties": return 1; + case "CLR Properties": return 2; + default: return 3; + } + } + } + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs similarity index 98% rename from src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs index 7e38749a6f..773140c446 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventOwnerTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventOwnerTreeNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels InputElement.PointerReleasedEvent, InputElement.PointerPressedEvent }; - public EventOwnerTreeNode(Type type, IEnumerable events, EventsViewModel vm) + public EventOwnerTreeNode(Type type, IEnumerable events, EventsPageViewModel vm) : base(null, type.Name) { Children = new AvaloniaList(events.OrderBy(e => e.Name) diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs similarity index 85% rename from src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs index 36f1904253..f017a50ea9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Diagnostics.Models; +using Avalonia.Diagnostics.Views; using Avalonia.Interactivity; using Avalonia.Threading; using Avalonia.VisualTree; @@ -12,11 +13,11 @@ namespace Avalonia.Diagnostics.ViewModels internal class EventTreeNode : EventTreeNodeBase { private readonly RoutedEvent _event; - private readonly EventsViewModel _parentViewModel; + private readonly EventsPageViewModel _parentViewModel; private bool _isRegistered; private FiredEvent _currentEvent; - public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsViewModel vm) + public EventTreeNode(EventOwnerTreeNode parent, RoutedEvent @event, EventsPageViewModel vm) : base(parent, @event.Name) { Contract.Requires(@event != null); @@ -65,7 +66,7 @@ namespace Avalonia.Diagnostics.ViewModels { if (!_isRegistered || IsEnabled == false) return; - if (sender is IVisual v && DevTools.BelongsToDevTool(v)) + if (sender is IVisual v && BelongsToDevTool(v)) return; var s = sender; @@ -94,5 +95,20 @@ namespace Avalonia.Diagnostics.ViewModels else handler(); } + + private static bool BelongsToDevTool(IVisual v) + { + while (v != null) + { + if (v is MainView || v is MainWindow) + { + return true; + } + + v = v.VisualParent; + } + + return false; + } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs similarity index 100% rename from src/Avalonia.Diagnostics/ViewModels/EventTreeNodeBase.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNodeBase.cs diff --git a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs similarity index 69% rename from src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs index 1c868148ce..0d58952687 100644 --- a/src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventsPageViewModel.cs @@ -12,12 +12,12 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.ViewModels { - internal class EventsViewModel : ViewModelBase, IDevToolViewModel + internal class EventsPageViewModel : ViewModelBase { private readonly IControl _root; private FiredEvent _selectedEvent; - public EventsViewModel(IControl root) + public EventsPageViewModel(IControl root) { _root = root; @@ -45,17 +45,4 @@ namespace Avalonia.Diagnostics.ViewModels RecordedEvents.Clear(); } } - - internal class BoolToBrushConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return (bool)value ? Brushes.Green : Brushes.Transparent; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } } diff --git a/src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs similarity index 100% rename from src/Avalonia.Diagnostics/ViewModels/FiredEvent.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs diff --git a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs similarity index 65% rename from src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs index 0b9bd85b4f..a7c2997346 100644 --- a/src/Avalonia.Diagnostics/ViewModels/LogicalTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/LogicalTreeNode.cs @@ -1,6 +1,3 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using Avalonia.Collections; using Avalonia.Controls; using Avalonia.LogicalTree; @@ -17,7 +14,8 @@ namespace Avalonia.Diagnostics.ViewModels public static LogicalTreeNode[] Create(object control) { - return control is ILogical logical ? new[] { new LogicalTreeNode(logical, null) } : null; + var logical = control as ILogical; + return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs new file mode 100644 index 0000000000..7addaba977 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -0,0 +1,126 @@ +using System; +using Avalonia.Controls; +using Avalonia.Diagnostics.Models; +using Avalonia.Input; + +namespace Avalonia.Diagnostics.ViewModels +{ + internal class MainViewModel : ViewModelBase, IDisposable + { + private readonly IControl _root; + private readonly TreePageViewModel _logicalTree; + private readonly TreePageViewModel _visualTree; + private readonly EventsPageViewModel _events; + private ViewModelBase _content; + private int _selectedTab; + private string _focusedControl; + private string _pointerOverElement; + + public MainViewModel(IControl root) + { + _root = root; + _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root)); + _visualTree = new TreePageViewModel(VisualTreeNode.Create(root)); + _events = new EventsPageViewModel(root); + + UpdateFocusedControl(); + KeyboardDevice.Instance.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) + { + UpdateFocusedControl(); + } + }; + + SelectedTab = 0; + root.GetObservable(TopLevel.PointerOverElementProperty) + .Subscribe(x => PointerOverElement = x?.GetType().Name); + Console = new ConsoleViewModel(UpdateConsoleContext); + } + + public ConsoleViewModel Console { get; } + + public ViewModelBase Content + { + get { return _content; } + private set + { + if (_content is TreePageViewModel oldTree && + value is TreePageViewModel newTree && + oldTree?.SelectedNode?.Visual is IControl control) + { + newTree.SelectControl(control); + } + + RaiseAndSetIfChanged(ref _content, value); + } + } + + public int SelectedTab + { + get { return _selectedTab; } + set + { + _selectedTab = value; + + switch (value) + { + case 0: + Content = _logicalTree; + break; + case 1: + Content = _visualTree; + break; + case 2: + Content = _events; + break; + } + + RaisePropertyChanged(); + } + } + + public string FocusedControl + { + get { return _focusedControl; } + private set { RaiseAndSetIfChanged(ref _focusedControl, value); } + } + + public string PointerOverElement + { + get { return _pointerOverElement; } + private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); } + } + + private void UpdateConsoleContext(ConsoleContext context) + { + context.root = _root; + + if (Content is TreePageViewModel tree) + { + context.e = tree.SelectedNode?.Visual; + } + } + + public void SelectControl(IControl control) + { + var tree = Content as TreePageViewModel; + + if (tree != null) + { + tree.SelectControl(control); + } + } + + public void Dispose() + { + _logicalTree.Dispose(); + _visualTree.Dispose(); + } + + private void UpdateFocusedControl() + { + FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs new file mode 100644 index 0000000000..7d284e1499 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Diagnostics.ViewModels +{ + internal abstract class PropertyViewModel : ViewModelBase + { + public abstract object Key { get; } + public abstract string Name { get; } + public abstract string Group { get; } + public abstract string Value { get; set; } + public abstract void Update(); + } +} diff --git a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs similarity index 81% rename from src/Avalonia.Diagnostics/ViewModels/TreeNode.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs index 902eb81bd9..7c403e1b04 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs @@ -27,9 +27,9 @@ namespace Avalonia.Diagnostics.ViewModels var classesChanged = Observable.FromEventPattern< NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( - x => styleable.Classes.CollectionChanged += x, - x => styleable.Classes.CollectionChanged -= x) - .TakeUntil(styleable.StyleDetach); + x => styleable.Classes.CollectionChanged += x, + x => styleable.Classes.CollectionChanged -= x) + .TakeUntil(((IStyleable)styleable).StyleDetach); classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) @@ -55,8 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels public string Classes { - get => _classes; - private set => RaiseAndSetIfChanged(ref _classes, value); + get { return _classes; } + private set { RaiseAndSetIfChanged(ref _classes, value); } } public IVisual Visual @@ -66,8 +66,8 @@ namespace Avalonia.Diagnostics.ViewModels public bool IsExpanded { - get => _isExpanded; - set => RaiseAndSetIfChanged(ref _isExpanded, value); + get { return _isExpanded; } + set { RaiseAndSetIfChanged(ref _isExpanded, value); } } public TreeNode Parent @@ -78,6 +78,7 @@ namespace Avalonia.Diagnostics.ViewModels public string Type { get; + private set; } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs similarity index 65% rename from src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs index b2b1aaa723..eddffb385b 100644 --- a/src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs @@ -1,24 +1,20 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - +using System; using Avalonia.Controls; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { - internal class TreePageViewModel : ViewModelBase, IDevToolViewModel + internal class TreePageViewModel : ViewModelBase, IDisposable { private TreeNode _selected; private ControlDetailsViewModel _details; + private string _propertyFilter; - public TreePageViewModel(TreeNode[] nodes, string name) + public TreePageViewModel(TreeNode[] nodes) { Nodes = nodes; - Name = name; } - public string Name { get; } - public TreeNode[] Nodes { get; protected set; } public TreeNode SelectedNode @@ -26,9 +22,16 @@ namespace Avalonia.Diagnostics.ViewModels get => _selected; set { + if (Details != null) + { + _propertyFilter = Details.PropertyFilter; + } + if (RaiseAndSetIfChanged(ref _selected, value)) { - Details = value != null ? new ControlDetailsViewModel(value.Visual) : null; + Details = value != null ? + new ControlDetailsViewModel(value.Visual, _propertyFilter) : + null; } } } @@ -36,9 +39,19 @@ namespace Avalonia.Diagnostics.ViewModels public ControlDetailsViewModel Details { get => _details; - private set => RaiseAndSetIfChanged(ref _details, value); + private set + { + var oldValue = _details; + + if (RaiseAndSetIfChanged(ref _details, value)) + { + oldValue?.Dispose(); + } + } } + public void Dispose() => _details?.Dispose(); + public TreeNode FindNode(IControl control) { foreach (var node in Nodes) @@ -66,7 +79,7 @@ namespace Avalonia.Diagnostics.ViewModels { control = control.GetVisualParent(); } - } + } if (node != null) { @@ -90,14 +103,16 @@ namespace Avalonia.Diagnostics.ViewModels { return node; } - - foreach (var child in node.Children) + else { - var result = FindNode(child, control); - - if (result != null) + foreach (var child in node.Children) { - return result; + var result = FindNode(child, control); + + if (result != null) + { + return result; + } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs similarity index 50% rename from src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs index a6ff4dd853..4e7d5f6604 100644 --- a/src/Avalonia.Diagnostics/ViewModels/ViewModelBase.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs @@ -1,34 +1,42 @@ -// Copyright (c) The Avalonia 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; using System.ComponentModel; using System.Runtime.CompilerServices; -using JetBrains.Annotations; namespace Avalonia.Diagnostics.ViewModels { - internal class ViewModelBase : INotifyPropertyChanged + public class ViewModelBase : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; + private PropertyChangedEventHandler _propertyChanged; + private List events = new List(); + + public event PropertyChangedEventHandler PropertyChanged + { + add { _propertyChanged += value; events.Add("added"); } + remove { _propertyChanged -= value; events.Add("removed"); } + } + + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + } - [NotifyPropertyChangedInvocator] protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) { if (!EqualityComparer.Default.Equals(field, value)) { field = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + RaisePropertyChanged(propertyName); return true; } return false; } - [NotifyPropertyChangedInvocator] protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + var e = new PropertyChangedEventArgs(propertyName); + OnPropertyChanged(e); + _propertyChanged?.Invoke(this, e); } } } diff --git a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs similarity index 85% rename from src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs rename to src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index 47ef91507a..8c070261d9 100644 --- a/src/Avalonia.Diagnostics/ViewModels/VisualTreeNode.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs @@ -29,11 +29,12 @@ namespace Avalonia.Diagnostics.ViewModels } } - public bool IsInTemplate { get; } + public bool IsInTemplate { get; private set; } public static VisualTreeNode[] Create(object control) { - return control is IVisual visual ? new[] { new VisualTreeNode(visual, null) } : null; + var visual = control as IVisual; + return visual != null ? new[] { new VisualTreeNode(visual, null) } : null; } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml new file mode 100644 index 0000000000..264a0de359 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + > + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs new file mode 100644 index 0000000000..ae70b59fde --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ConsoleView.xaml.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.Views +{ + internal class ConsoleView : UserControl + { + private readonly ListBox _historyList; + private readonly TextBox _input; + + public ConsoleView() + { + this.InitializeComponent(); + _historyList = this.FindControl("historyList"); + ((ILogical)_historyList).LogicalChildren.CollectionChanged += HistoryChanged; + _input = this.FindControl("input"); + _input.KeyDown += InputKeyDown; + } + + public void FocusInput() => _input.Focus(); + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void HistoryChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems[0] is IControl control) + { + DispatcherTimer.RunOnce(control.BringIntoView, TimeSpan.Zero); + } + } + + private void InputKeyDown(object sender, KeyEventArgs e) + { + var vm = (ConsoleViewModel)DataContext; + + switch (e.Key) + { + case Key.Enter: + vm.Execute(); + e.Handled = true; + break; + case Key.Up: + vm.HistoryUp(); + _input.CaretIndex = _input.Text.Length; + e.Handled = true; + break; + case Key.Down: + vm.HistoryDown(); + _input.CaretIndex = _input.Text.Length; + e.Handled = true; + break; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml new file mode 100644 index 0000000000..903f18aa19 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs new file mode 100644 index 0000000000..c6bd5a18aa --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Avalonia.Diagnostics.Views +{ + internal class ControlDetailsView : UserControl + { + public ControlDetailsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/EventsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml similarity index 92% rename from src/Avalonia.Diagnostics/Views/EventsView.xaml rename to src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml index 406dd433a2..b7f0860e70 100644 --- a/src/Avalonia.Diagnostics/Views/EventsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/EventsPageView.xaml @@ -1,9 +1,10 @@  + xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters" + x:Class="Avalonia.Diagnostics.Views.EventsPageView"> - + ("events"); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml new file mode 100644 index 0000000000..663722acba --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Hold Ctrl+Shift over a control to inspect. + + Focused: + + + Pointer Over: + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs new file mode 100644 index 0000000000..783709e54b --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; + +namespace Avalonia.Diagnostics.Views +{ + internal class MainView : UserControl + { + private readonly ConsoleView _console; + private readonly GridSplitter _consoleSplitter; + private readonly Grid _rootGrid; + private readonly int _consoleRow; + private double _consoleHeight = -1; + + public MainView() + { + InitializeComponent(); + AddHandler(KeyDownEvent, PreviewKeyDown, RoutingStrategies.Tunnel); + _console = this.FindControl("console"); + _consoleSplitter = this.FindControl("consoleSplitter"); + _rootGrid = this.FindControl("rootGrid"); + _consoleRow = Grid.GetRow(_console); + } + + public void ToggleConsole() + { + var vm = (MainViewModel)DataContext; + + if (_consoleHeight == -1) + { + _consoleHeight = Bounds.Height / 3; + } + + vm.Console.ToggleVisibility(); + _consoleSplitter.IsVisible = vm.Console.IsVisible; + + if (vm.Console.IsVisible) + { + _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(_consoleHeight, GridUnitType.Pixel); + Dispatcher.UIThread.Post(() => _console.FocusInput(), DispatcherPriority.Background); + } + else + { + _consoleHeight = _rootGrid.RowDefinitions[_consoleRow].Height.Value; + _rootGrid.RowDefinitions[_consoleRow].Height = new GridLength(0, GridUnitType.Pixel); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + ToggleConsole(); + e.Handled = true; + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml new file mode 100644 index 0000000000..3623e95597 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs new file mode 100644 index 0000000000..3abdb5034a --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Diagnostics.ViewModels; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using Avalonia.VisualTree; + +namespace Avalonia.Diagnostics.Views +{ + internal class MainWindow : Window, IStyleHost + { + private TopLevel _root; + private IDisposable _keySubscription; + + public MainWindow() + { + InitializeComponent(); + + _keySubscription = InputManager.Instance.Process + .OfType() + .Subscribe(RawKeyDown); + } + + public TopLevel Root + { + get => _root; + set + { + if (_root != value) + { + _root = value; + DataContext = new MainViewModel(value); + } + } + } + + IStyleHost IStyleHost.StylingParent => null; + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _keySubscription.Dispose(); + ((MainViewModel)DataContext)?.Dispose(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void RawKeyDown(RawKeyEventArgs e) + { + const RawInputModifiers modifiers = RawInputModifiers.Control | RawInputModifiers.Shift; + + if (e.Modifiers == modifiers) + { + var point = (Root as IInputRoot)?.MouseDevice?.GetPosition(Root) ?? default; + var control = Root.GetVisualsAt(point, x => (!(x is AdornerLayer) && x.IsVisible)) + .FirstOrDefault(); + + if (control != null) + { + var vm = (MainViewModel)DataContext; + vm.SelectControl((IControl)control); + } + } + } + } +} diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml similarity index 60% rename from src/Avalonia.Diagnostics/Views/TreePageView.xaml rename to src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml index 2619fd744a..a1e6ca7d37 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml @@ -1,26 +1,29 @@ - - + + - - + + - - + + diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs similarity index 90% rename from src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs rename to src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs index 1326f718de..633d18ddd8 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs @@ -1,6 +1,3 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using Avalonia.Controls; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; @@ -12,14 +9,14 @@ using Avalonia.Media; namespace Avalonia.Diagnostics.Views { - public class TreePageView : UserControl + internal class TreePageView : UserControl { private Control _adorner; private TreeView _tree; public TreePageView() { - InitializeComponent(); + this.InitializeComponent(); _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized; } @@ -39,7 +36,7 @@ namespace Avalonia.Diagnostics.Views _adorner = new Rectangle { Fill = new SolidColorBrush(0x80a0c5e8), - [AdornerLayer.AdornedElementProperty] = node.Visual + [AdornerLayer.AdornedElementProperty] = node.Visual, }; layer.Children.Add(_adorner); diff --git a/src/Avalonia.Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs similarity index 100% rename from src/Avalonia.Diagnostics/VisualTreeDebug.cs rename to src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs diff --git a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs deleted file mode 100644 index 4b832f7ce6..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/ControlDetailsViewModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using System.Linq; -using Avalonia.VisualTree; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class ControlDetailsViewModel : ViewModelBase - { - public ControlDetailsViewModel(IVisual control) - { - if (control is AvaloniaObject avaloniaObject) - { - Properties = AvaloniaPropertyRegistry.Instance.GetRegistered(avaloniaObject) - .Select(x => new PropertyDetails(avaloniaObject, x)) - .OrderBy(x => x.IsAttached) - .ThenBy(x => x.Name); - } - } - - public IEnumerable Properties { get; } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs deleted file mode 100644 index 9f524a21eb..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) The Avalonia 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.ObjectModel; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Input; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class DevToolsViewModel : ViewModelBase - { - private IDevToolViewModel _selectedTool; - private string _focusedControl; - private string _pointerOverElement; - - public DevToolsViewModel(IControl root) - { - Tools = new ObservableCollection - { - new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"), - new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"), - new EventsViewModel(root) - }; - - SelectedTool = Tools.First(); - - UpdateFocusedControl(); - - KeyboardDevice.Instance.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) - { - UpdateFocusedControl(); - } - }; - - root.GetObservable(TopLevel.PointerOverElementProperty) - .Subscribe(x => PointerOverElement = x?.GetType().Name); - } - - public IDevToolViewModel SelectedTool - { - get => _selectedTool; - set => RaiseAndSetIfChanged(ref _selectedTool, value); - } - - public ObservableCollection Tools { get; } - - public string FocusedControl - { - get => _focusedControl; - private set => RaiseAndSetIfChanged(ref _focusedControl, value); - } - - public string PointerOverElement - { - get => _pointerOverElement; - private set => RaiseAndSetIfChanged(ref _pointerOverElement, value); - } - - public void SelectControl(IControl control) - { - if (SelectedTool is TreePageViewModel tree) - { - tree.SelectControl(control); - } - } - - private void UpdateFocusedControl() - { - FocusedControl = KeyboardDevice.Instance.FocusedElement?.GetType().Name; - } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs deleted file mode 100644 index 0434230a63..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Avalonia.Diagnostics.ViewModels -{ - /// - /// View model interface for tool showing up in DevTools - /// - public interface IDevToolViewModel - { - /// - /// Name of a tool. - /// - string Name { get; } - } -} diff --git a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs b/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs deleted file mode 100644 index 523be406c8..0000000000 --- a/src/Avalonia.Diagnostics/ViewModels/PropertyDetails.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Data; - -namespace Avalonia.Diagnostics.ViewModels -{ - internal class PropertyDetails : ViewModelBase - { - private object _value; - private string _priority; - private string _diagnostic; - - public PropertyDetails(AvaloniaObject o, AvaloniaProperty property) - { - Name = property.IsAttached ? - $"[{property.OwnerType.Name}.{property.Name}]" : - property.Name; - IsAttached = property.IsAttached; - - // TODO: Unsubscribe when view model is deactivated. - o.GetObservable(property).Subscribe(x => - { - var diagnostic = o.GetDiagnostic(property); - Value = diagnostic.Value ?? "(null)"; - Priority = (diagnostic.Priority != BindingPriority.Unset) ? - diagnostic.Priority.ToString() : - diagnostic.Property.Inherits ? - "Inherited" : - "Unset"; - Diagnostic = diagnostic.Diagnostic; - }); - } - - public string Name { get; } - - public bool IsAttached { get; } - - public string Priority - { - get => _priority; - private set => RaiseAndSetIfChanged(ref _priority, value); - } - - public string Diagnostic - { - get => _diagnostic; - private set => RaiseAndSetIfChanged(ref _diagnostic, value); - } - - public object Value - { - get => _value; - private set => RaiseAndSetIfChanged(ref _value, value); - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs b/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs deleted file mode 100644 index fb867ab55e..0000000000 --- a/src/Avalonia.Diagnostics/Views/ControlDetailsView.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) The Avalonia 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; -using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.Diagnostics.ViewModels; -using Avalonia.Media; - -namespace Avalonia.Diagnostics.Views -{ - internal class ControlDetailsView : UserControl - { - private static readonly StyledProperty ViewModelProperty = - AvaloniaProperty.Register(nameof(ViewModel)); - - private SimpleGrid _grid; - - public ControlDetailsView() - { - InitializeComponent(); - this.GetObservable(DataContextProperty) - .Subscribe(x => ViewModel = (ControlDetailsViewModel)x); - } - - public ControlDetailsViewModel ViewModel - { - get => GetValue(ViewModelProperty); - private set - { - SetValue(ViewModelProperty, value); - _grid[GridRepeater.ItemsProperty] = value?.Properties; - } - } - - private void InitializeComponent() - { - Func> pt = PropertyTemplate; - - Content = new ScrollViewer { Content = _grid = new SimpleGrid { [GridRepeater.TemplateProperty] = pt } }; - } - - private IEnumerable PropertyTemplate(object i) - { - var property = (PropertyDetails)i; - - var margin = new Thickness(2); - - yield return new TextBlock - { - Margin = margin, - Text = property.Name, - TextWrapping = TextWrapping.NoWrap, - [!ToolTip.TipProperty] = property.GetObservable(nameof(property.Diagnostic)).ToBinding() - }; - - yield return new TextBlock - { - Margin = margin, - TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable(nameof(property.Value)) - .Select(v => v?.ToString()) - .ToBinding() - }; - - yield return new TextBlock - { - Margin = margin, - TextWrapping = TextWrapping.NoWrap, - [!TextBlock.TextProperty] = property.GetObservable((nameof(property.Priority))).ToBinding() - }; - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/GridRepeater.cs b/src/Avalonia.Diagnostics/Views/GridRepeater.cs deleted file mode 100644 index b0ff26c7b6..0000000000 --- a/src/Avalonia.Diagnostics/Views/GridRepeater.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) The Avalonia 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; -using System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Diagnostics.Views -{ - internal static class GridRepeater - { - public static readonly AttachedProperty ItemsProperty = - AvaloniaProperty.RegisterAttached("Items", typeof(GridRepeater)); - - public static readonly AttachedProperty>> TemplateProperty = - AvaloniaProperty.RegisterAttached>>("Template", - typeof(GridRepeater)); - - static GridRepeater() - { - ItemsProperty.Changed.Subscribe(ItemsChanged); - } - - private static void ItemsChanged(AvaloniaPropertyChangedEventArgs e) - { - var grid = (SimpleGrid)e.Sender; - var items = (IEnumerable)e.NewValue; - var template = grid.GetValue(TemplateProperty); - - grid.Children.Clear(); - - if (items != null) - { - int count = 0; - int cols = 3; - - foreach (var item in items) - { - foreach (var control in template(item)) - { - grid.Children.Add(control); - SimpleGrid.SetColumn(control, count % cols); - SimpleGrid.SetRow(control, count / cols); - ++count; - } - } - } - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs b/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs deleted file mode 100644 index 24b2f29463..0000000000 --- a/src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) The Avalonia 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.ComponentModel; -using System.Reactive.Linq; -using System.Reflection; - -namespace Avalonia.Diagnostics.Views -{ - internal static class PropertyChangedExtensions - { - public static IObservable GetObservable(this INotifyPropertyChanged source, string propertyName) - { - Contract.Requires(source != null); - Contract.Requires(propertyName != null); - - var property = source.GetType().GetTypeInfo().GetDeclaredProperty(propertyName); - - if (property == null) - { - throw new ArgumentException($"Property '{propertyName}' not found on '{source}."); - } - - return Observable.FromEventPattern( - e => source.PropertyChanged += e, - e => source.PropertyChanged -= e) - .Where(e => e.EventArgs.PropertyName == propertyName) - .Select(_ => (T)property.GetValue(source)) - .StartWith((T)property.GetValue(source)); - } - } -} diff --git a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs b/src/Avalonia.Diagnostics/Views/SimpleGrid.cs deleted file mode 100644 index 4fc77666e1..0000000000 --- a/src/Avalonia.Diagnostics/Views/SimpleGrid.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; -using Avalonia.Controls; - -namespace Avalonia.Diagnostics.Views -{ - /// - /// A simple grid control that lays out columns with a equal width and rows to their desired - /// size. - /// - /// - /// This is used in the devtools because our performance sucks. - /// - public class SimpleGrid : Panel - { - private readonly List _columnWidths = new List(); - private readonly List _rowHeights = new List(); - private double _totalWidth; - private double _totalHeight; - - /// - /// Defines the Column attached property. - /// - public static readonly AttachedProperty ColumnProperty = - AvaloniaProperty.RegisterAttached("Column"); - - /// - /// Defines the Row attached property. - /// - public static readonly AttachedProperty RowProperty = - AvaloniaProperty.RegisterAttached("Row"); - - /// - /// Gets the value of the Column attached property for a control. - /// - /// The control. - /// The control's column. - public static int GetColumn(IControl control) - { - return control.GetValue(ColumnProperty); - } - - /// - /// Gets the value of the Row attached property for a control. - /// - /// The control. - /// The control's row. - public static int GetRow(IControl control) - { - return control.GetValue(RowProperty); - } - - /// - /// Sets the value of the Column attached property for a control. - /// - /// The control. - /// The column value. - public static void SetColumn(IControl control, int value) - { - control.SetValue(ColumnProperty, value); - } - - - /// - /// Sets the value of the Row attached property for a control. - /// - /// The control. - /// The row value. - public static void SetRow(IControl control, int value) - { - control.SetValue(RowProperty, value); - } - - protected override Size MeasureOverride(Size availableSize) - { - _columnWidths.Clear(); - _rowHeights.Clear(); - _totalWidth = 0; - _totalHeight = 0; - - foreach (var child in Children) - { - var column = GetColumn(child); - var row = GetRow(child); - - child.Measure(availableSize); - - var desired = child.DesiredSize; - UpdateCell(_columnWidths, column, desired.Width, ref _totalWidth); - UpdateCell(_rowHeights, row, desired.Height, ref _totalHeight); - } - - return new Size(_totalWidth, _totalHeight); - } - - protected override Size ArrangeOverride(Size finalSize) - { - var columnWidth = finalSize.Width / _columnWidths.Count; - - foreach (var child in Children) - { - var column = GetColumn(child); - var row = GetRow(child); - var rect = new Rect(column * columnWidth, GetRowTop(row), columnWidth, _rowHeights[row]); - child.Arrange(rect); - } - - return new Size(finalSize.Width, _totalHeight); - } - - private double UpdateCell(IList cells, int cell, double value, ref double total) - { - while (cells.Count < cell + 1) - { - cells.Add(0); - } - - var existing = cells[cell]; - - if (value > existing) - { - cells[cell] = value; - total += value - existing; - return value; - } - else - { - return existing; - } - } - - private double GetRowTop(int row) - { - var result = 0.0; - - for (var i = 0; i < row; ++i) - { - result += _rowHeights[i]; - } - - return result; - } - } -}