From a45dd094a08e7ee09d07d5e928fc756daae046c3 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Thu, 15 Jul 2021 00:20:08 +0200 Subject: [PATCH 001/270] Added first visual-scripting draft [WIP] --- src/Artemis.Core/VisualScripting/INode.cs | 26 + src/Artemis.Core/VisualScripting/IPin.cs | 20 + .../VisualScripting/IPinCollection.cs | 16 + src/Artemis.Core/VisualScripting/IScript.cs | 25 + .../VisualScripting/PinDirection.cs | 8 + src/Artemis.UI/App.xaml | 7 +- src/Artemis.UI/Artemis.UI.csproj | 1 + .../DisplayConditionsView.xaml | 7 +- .../DisplayConditionsViewModel.cs | 66 +- src/Artemis.UI/packages.lock.json | 12 + .../Artemis.VisualScripting.csproj | 52 + .../Attributes/UIAttribute.cs | 37 + .../Controls/VisualScriptCablePresenter.cs | 68 + .../Editor/Controls/VisualScriptEditor.cs | 41 + .../Controls/VisualScriptNodeCreationBox.cs | 115 ++ .../Controls/VisualScriptNodePresenter.cs | 137 ++ .../Controls/VisualScriptPinPresenter.cs | 184 +++ .../Editor/Controls/VisualScriptPresenter.cs | 437 ++++++ .../Editor/Controls/Wrapper/VisualScript.cs | 189 +++ .../Controls/Wrapper/VisualScriptCable.cs | 67 + .../Controls/Wrapper/VisualScriptNode.cs | 179 +++ .../Controls/Wrapper/VisualScriptPin.cs | 112 ++ .../Wrapper/VisualScriptPinCollection.cs | 58 + .../Editor/EditorStyles.xaml | 13 + .../Styles/VisualScriptCablePresenter.xaml | 68 + .../Editor/Styles/VisualScriptEditor.xaml | 28 + .../Styles/VisualScriptNodeCreationBox.xaml | 54 + .../Styles/VisualScriptNodePresenter.xaml | 262 ++++ .../Styles/VisualScriptPinPresenter.xaml | 113 ++ .../Editor/Styles/VisualScriptPresenter.xaml | 111 ++ .../Events/PinConnectedEventArgs.cs | 25 + .../Events/PinDisconnectedEventArgs.cs | 25 + .../VisualScriptNodeDragMovingEventArgs.cs | 24 + ...ualScriptNodeIsSelectedChangedEventArgs.cs | 24 + .../Internal/ExitNode.cs | 40 + .../Internal/IsConnectingPin.cs | 26 + src/Artemis.VisualScripting/Model/InputPin.cs | 51 + .../Model/InputPinCollection.cs | 35 + src/Artemis.VisualScripting/Model/Node.cs | 117 ++ src/Artemis.VisualScripting/Model/NodeData.cs | 38 + .../Model/OutputPin.cs | 41 + .../Model/OutputPinCollection.cs | 29 + src/Artemis.VisualScripting/Model/Pin.cs | 64 + .../Model/PinCollection.cs | 55 + src/Artemis.VisualScripting/Model/Script.cs | 91 ++ .../Nodes/BoolOperations.cs | 160 +++ .../Nodes/ConvertNodes.cs | 107 ++ .../Nodes/StaticValueNodes.cs | 121 ++ .../Nodes/StringFormatNode.cs | 40 + .../Nodes/Styles/StaticValueNodes.xaml | 12 + src/Artemis.VisualScripting/Nodes/SumNode.cs | 70 + .../Services/NodeService.cs | 59 + .../ViewModel/AbstractBindable.cs | 64 + .../ViewModel/ActionCommand.cs | 77 + .../packages.lock.json | 1246 +++++++++++++++++ src/Artemis.sln | 6 + 56 files changed, 5139 insertions(+), 21 deletions(-) create mode 100644 src/Artemis.Core/VisualScripting/INode.cs create mode 100644 src/Artemis.Core/VisualScripting/IPin.cs create mode 100644 src/Artemis.Core/VisualScripting/IPinCollection.cs create mode 100644 src/Artemis.Core/VisualScripting/IScript.cs create mode 100644 src/Artemis.Core/VisualScripting/PinDirection.cs create mode 100644 src/Artemis.VisualScripting/Artemis.VisualScripting.csproj create mode 100644 src/Artemis.VisualScripting/Attributes/UIAttribute.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs create mode 100644 src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs create mode 100644 src/Artemis.VisualScripting/Editor/EditorStyles.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptEditor.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodeCreationBox.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptPinPresenter.xaml create mode 100644 src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml create mode 100644 src/Artemis.VisualScripting/Events/PinConnectedEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Events/PinDisconnectedEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Events/VisualScriptNodeDragMovingEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Events/VisualScriptNodeIsSelectedChangedEventArgs.cs create mode 100644 src/Artemis.VisualScripting/Internal/ExitNode.cs create mode 100644 src/Artemis.VisualScripting/Internal/IsConnectingPin.cs create mode 100644 src/Artemis.VisualScripting/Model/InputPin.cs create mode 100644 src/Artemis.VisualScripting/Model/InputPinCollection.cs create mode 100644 src/Artemis.VisualScripting/Model/Node.cs create mode 100644 src/Artemis.VisualScripting/Model/NodeData.cs create mode 100644 src/Artemis.VisualScripting/Model/OutputPin.cs create mode 100644 src/Artemis.VisualScripting/Model/OutputPinCollection.cs create mode 100644 src/Artemis.VisualScripting/Model/Pin.cs create mode 100644 src/Artemis.VisualScripting/Model/PinCollection.cs create mode 100644 src/Artemis.VisualScripting/Model/Script.cs create mode 100644 src/Artemis.VisualScripting/Nodes/BoolOperations.cs create mode 100644 src/Artemis.VisualScripting/Nodes/ConvertNodes.cs create mode 100644 src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs create mode 100644 src/Artemis.VisualScripting/Nodes/StringFormatNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Styles/StaticValueNodes.xaml create mode 100644 src/Artemis.VisualScripting/Nodes/SumNode.cs create mode 100644 src/Artemis.VisualScripting/Services/NodeService.cs create mode 100644 src/Artemis.VisualScripting/ViewModel/AbstractBindable.cs create mode 100644 src/Artemis.VisualScripting/ViewModel/ActionCommand.cs create mode 100644 src/Artemis.VisualScripting/packages.lock.json diff --git a/src/Artemis.Core/VisualScripting/INode.cs b/src/Artemis.Core/VisualScripting/INode.cs new file mode 100644 index 000000000..d304f179e --- /dev/null +++ b/src/Artemis.Core/VisualScripting/INode.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Artemis.Core.VisualScripting +{ + public interface INode : INotifyPropertyChanged + { + string Name { get; } + string Description { get; } + + public double X { get; set; } + public double Y { get; set; } + + public IReadOnlyCollection Pins { get; } + public IReadOnlyCollection PinCollections { get; } + + public object CustomView { get; } + public object CustomViewModel { get; } + + event EventHandler Resetting; + + void Evaluate(); + void Reset(); + } +} diff --git a/src/Artemis.Core/VisualScripting/IPin.cs b/src/Artemis.Core/VisualScripting/IPin.cs new file mode 100644 index 000000000..9b2cb9245 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/IPin.cs @@ -0,0 +1,20 @@ +using System; +using Artemis.VisualScripting.Model; + +namespace Artemis.Core.VisualScripting +{ + public interface IPin + { + INode Node { get; } + + string Name { get; } + PinDirection Direction { get; } + Type Type { get; } + object PinValue { get; } + + bool IsEvaluated { get; set; } + + void ConnectTo(IPin pin); + void DisconnectFrom(IPin pin); + } +} diff --git a/src/Artemis.Core/VisualScripting/IPinCollection.cs b/src/Artemis.Core/VisualScripting/IPinCollection.cs new file mode 100644 index 000000000..d9c2a1b33 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/IPinCollection.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Artemis.VisualScripting.Model; + +namespace Artemis.Core.VisualScripting +{ + public interface IPinCollection : IEnumerable + { + string Name { get; } + PinDirection Direction { get; } + Type Type { get; } + + IPin AddPin(); + bool Remove(IPin pin); + } +} diff --git a/src/Artemis.Core/VisualScripting/IScript.cs b/src/Artemis.Core/VisualScripting/IScript.cs new file mode 100644 index 000000000..abcc13516 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/IScript.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Artemis.Core.VisualScripting +{ + public interface IScript : INotifyPropertyChanged, IDisposable + { + string Name { get; } + string Description { get; } + + IEnumerable Nodes { get; } + + Type ResultType { get; } + + void Run(); + void AddNode(INode node); + void RemoveNode(INode node); + } + + public interface IScript : IScript + { + T Result { get; } + } +} diff --git a/src/Artemis.Core/VisualScripting/PinDirection.cs b/src/Artemis.Core/VisualScripting/PinDirection.cs new file mode 100644 index 000000000..cc4b8500b --- /dev/null +++ b/src/Artemis.Core/VisualScripting/PinDirection.cs @@ -0,0 +1,8 @@ +namespace Artemis.VisualScripting.Model +{ + public enum PinDirection + { + Input, + Output + } +} diff --git a/src/Artemis.UI/App.xaml b/src/Artemis.UI/App.xaml index 1e96c39a4..f23a62a35 100644 --- a/src/Artemis.UI/App.xaml +++ b/src/Artemis.UI/App.xaml @@ -34,16 +34,19 @@ - + + + + - + pack://application:,,,/Resources/Fonts/#Roboto Mono diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 372979bc4..c7a744e8a 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -82,6 +82,7 @@ true + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index be801816d..0261dc31c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -8,6 +8,7 @@ xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:displayConditions="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayConditions" xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" + xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting" x:Class="Artemis.UI.Screens.ProfileEditor.DisplayConditions.DisplayConditionsView" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" @@ -40,7 +41,9 @@ - + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index fad56c3dc..472c38dc1 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using Artemis.Core; -using Artemis.Core.Modules; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions @@ -24,6 +24,35 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { _profileEditorService = profileEditorService; _dataModelConditionsVmFactory = dataModelConditionsVmFactory; + + AvailableNodes = _nodeService.AvailableNodes; + Script = new Script("Display Condition (TODO)", "TODO"); + } + + #region TODO + + private static NodeService _nodeService; + + static DisplayConditionsViewModel() + { + _nodeService = new NodeService(); + _nodeService.InitializeNodes(); + } + + #endregion + + private Script _script; + public Script Script + { + get => _script; + private set => SetAndNotify(ref _script, value); + } + + private IEnumerable _availableNodes; + public IEnumerable AvailableNodes + { + get => _availableNodes; + set => SetAndNotify(ref _availableNodes, value); } public bool DisplayStartHint @@ -110,8 +139,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { if (RenderProfileElement != null) { - RenderProfileElement.DisplayCondition.ChildAdded -= DisplayConditionOnChildrenModified; - RenderProfileElement.DisplayCondition.ChildRemoved -= DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildAdded -= DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildRemoved -= DisplayConditionOnChildrenModified; RenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; } @@ -128,23 +157,26 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions } // Ensure the layer has a root display condition group + //if (renderProfileElement.DisplayCondition == null) + // renderProfileElement.DisplayCondition = new DataModelConditionGroup(null); + if (renderProfileElement.DisplayCondition == null) - renderProfileElement.DisplayCondition = new DataModelConditionGroup(null); + renderProfileElement.DisplayCondition = new Script("Display Condition (TODO)", "-"); - List modules = new(); - if (_profileEditorService.SelectedProfileConfiguration?.Module != null) - modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); - ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(renderProfileElement.DisplayCondition, ConditionGroupType.General, modules); - ActiveItem.IsRootGroup = true; + //List modules = new(); + //if (_profileEditorService.SelectedProfileConfiguration?.Module != null) + // modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); + //ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(renderProfileElement.DisplayCondition, ConditionGroupType.General, modules); + //ActiveItem.IsRootGroup = true; - DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); - IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); + //DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); + //IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); - RenderProfileElement.DisplayCondition.ChildAdded += DisplayConditionOnChildrenModified; - RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildAdded += DisplayConditionOnChildrenModified; + //RenderProfileElement.DisplayCondition.ChildRemoved += DisplayConditionOnChildrenModified; RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; } - + private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) { NotifyOfPropertyChange(nameof(DisplayContinuously)); @@ -154,8 +186,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void DisplayConditionOnChildrenModified(object sender, EventArgs e) { - DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); - IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); + //DisplayStartHint = !RenderProfileElement.DisplayCondition.Children.Any(); + //IsEventCondition = RenderProfileElement.DisplayCondition.Children.Any(c => c is DataModelConditionEvent); } public void EventTriggerModeSelected() diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index ef2b15f3e..88c76e6f1 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -247,6 +247,11 @@ "resolved": "2.1.0", "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2021.1.0", + "contentHash": "n9JSw5Z+F+6gp9vSv4aLH6p/bx3GAYA6FZVq1wJq/TJySv/kPgFKLGFeS7A8Xa5X4/GWorh5gd43yjamUgnBNA==" + }, "LiteDB": { "type": "Transitive", "resolved": "5.0.10", @@ -1472,6 +1477,13 @@ "System.Buffers": "4.5.1", "System.Numerics.Vectors": "4.5.0" } + }, + "artemis.visualscripting": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "JetBrains.Annotations": "2021.1.0" + } } } } diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj new file mode 100644 index 000000000..86031c078 --- /dev/null +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -0,0 +1,52 @@ + + + net5.0-windows + false + false + Artemis.VisualScripting + Artemis Visual-Scripting + Copyright © Darth Affe - 2021 + bin\ + x64 + true + disable + latest + + + + x64 + bin\Artemis.VisualScripting.xml + + 5 + + + + 1.0-{chash:6} + true + true + true + v[0-9]* + true + git + true + + + + bin\Artemis.VisualScripting.xml + + + + + + + + + + + + + $(DefaultXamlRuntime) + Designer + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Attributes/UIAttribute.cs b/src/Artemis.VisualScripting/Attributes/UIAttribute.cs new file mode 100644 index 000000000..86421c9d4 --- /dev/null +++ b/src/Artemis.VisualScripting/Attributes/UIAttribute.cs @@ -0,0 +1,37 @@ +using System; + +namespace Artemis.VisualScripting.Attributes +{ + public class UIAttribute : Attribute + { + #region Properties & Fields + + public string Name { get; } + public string Description { get; set; } + public string Category { get; set; } + + #endregion + + #region Constructors + + public UIAttribute(string name) + { + this.Name = name; + } + + public UIAttribute(string name, string description) + { + this.Name = name; + this.Description = description; + } + + public UIAttribute(string name, string description, string category) + { + this.Name = name; + this.Description = description; + this.Category = category; + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs new file mode 100644 index 000000000..32de5d316 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs @@ -0,0 +1,68 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Shapes; +using Artemis.VisualScripting.Editor.Controls.Wrapper; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_PATH, Type = typeof(Path))] + public class VisualScriptCablePresenter : Control + { + #region Constants + + private const string PART_PATH = "PART_Path"; + + #endregion + + #region Properties & Fields + + private Path _path; + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty CableProperty = DependencyProperty.Register( + "Cable", typeof(VisualScriptCable), typeof(VisualScriptCablePresenter), new PropertyMetadata(default(VisualScriptCable))); + + public VisualScriptCable Cable + { + get => (VisualScriptCable)GetValue(CableProperty); + set => SetValue(CableProperty, value); + } + + public static readonly DependencyProperty ThicknessProperty = DependencyProperty.Register( + "Thickness", typeof(double), typeof(VisualScriptCablePresenter), new PropertyMetadata(default(double))); + + public double Thickness + { + get => (double)GetValue(ThicknessProperty); + set => SetValue(ThicknessProperty, value); + } + + #endregion + + #region Methods + + public override void OnApplyTemplate() + { + _path = GetTemplateChild(PART_PATH) as Path ?? throw new NullReferenceException($"The Path '{PART_PATH}' is missing."); + _path.MouseDown += OnPathMouseDown; + } + + private void OnPathMouseDown(object sender, MouseButtonEventArgs args) + { + if ((args.ChangedButton == MouseButton.Left) && (args.LeftButton == MouseButtonState.Pressed) && (args.ClickCount == 2)) + { + //TODO DarthAffe 17.06.2021: Should we add rerouting? + //AddRerouteNode(); + } + else if (args.ChangedButton == MouseButton.Middle) + Cable.Disconnect(); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs new file mode 100644 index 000000000..dad4ef715 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Model; + +namespace Artemis.VisualScripting.Editor.Controls +{ + public class VisualScriptEditor : Control + { + #region Properties & Fields + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty ScriptProperty = DependencyProperty.Register( + "Script", typeof(IScript), typeof(VisualScriptEditor), new PropertyMetadata(default(IScript))); + + public IScript Script + { + get => (IScript)GetValue(ScriptProperty); + set => SetValue(ScriptProperty, value); + } + + public static readonly DependencyProperty AvailableNodesProperty = DependencyProperty.Register( + "AvailableNodes", typeof(IEnumerable), typeof(VisualScriptEditor), new PropertyMetadata(default(IEnumerable))); + + public IEnumerable AvailableNodes + { + get => (IEnumerable)GetValue(AvailableNodesProperty); + set => SetValue(AvailableNodesProperty, value); + } + + #endregion + + #region Methods + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs new file mode 100644 index 000000000..6019a7811 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using Artemis.VisualScripting.Model; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_SEARCHBOX, Type = typeof(TextBox))] + [TemplatePart(Name = PART_CONTENT, Type = typeof(ListBox))] + public class VisualScriptNodeCreationBox : Control + { + #region Constants + + private const string PART_SEARCHBOX = "PART_SearchBox"; + private const string PART_CONTENT = "PART_Content"; + + #endregion + + #region Properties & Fields + + private TextBox _searchBox; + private ListBox _contentList; + + private CollectionViewSource _collectionViewSource; + private ICollectionView _contentView; + + #endregion + + #region DependencyProperties + + public static readonly DependencyProperty AvailableNodesProperty = DependencyProperty.Register( + "AvailableNodes", typeof(IEnumerable), typeof(VisualScriptNodeCreationBox), new PropertyMetadata(default(IEnumerable), OnItemsSourceChanged)); + + public IEnumerable AvailableNodes + { + get => (IEnumerable)GetValue(AvailableNodesProperty); + set => SetValue(AvailableNodesProperty, value); + } + + public static readonly DependencyProperty CreateNodeCommandProperty = DependencyProperty.Register( + "CreateNodeCommand", typeof(ICommand), typeof(VisualScriptNodeCreationBox), new PropertyMetadata(default(ICommand))); + + public ICommand CreateNodeCommand + { + get => (ICommand)GetValue(CreateNodeCommandProperty); + set => SetValue(CreateNodeCommandProperty, value); + } + + #endregion + + #region Methods + + public override void OnApplyTemplate() + { + _searchBox = GetTemplateChild(PART_SEARCHBOX) as TextBox ?? throw new NullReferenceException($"The Element '{PART_SEARCHBOX}' is missing."); + _contentList = GetTemplateChild(PART_CONTENT) as ListBox ?? throw new NullReferenceException($"The Element '{PART_CONTENT}' is missing."); + + _searchBox.TextChanged += OnSearchBoxTextChanged; + _contentList.IsSynchronizedWithCurrentItem = false; + _contentList.SelectionChanged += OnContentListSelectionChanged; + _contentList.SelectionMode = SelectionMode.Single; + + _searchBox.Focus(); + _contentView?.Refresh(); + ItemsSourceChanged(); + } + + private void OnSearchBoxTextChanged(object sender, TextChangedEventArgs args) + { + _contentView?.Refresh(); + } + + private void OnContentListSelectionChanged(object sender, SelectionChangedEventArgs args) + { + if ((args == null) || (_contentList?.SelectedItem == null)) return; + + CreateNodeCommand?.Execute(_contentList.SelectedItem); + } + + private bool Filter(object o) + { + if (_searchBox == null) return false; + if (o is not NodeData nodeData) return false; + + return nodeData.Name.Contains(_searchBox.Text, StringComparison.OrdinalIgnoreCase); + } + + private void ItemsSourceChanged() + { + if (_contentList == null) return; + + if (AvailableNodes == null) + { + _contentView = null; + _collectionViewSource = null; + } + else + { + _collectionViewSource = new CollectionViewSource { Source = AvailableNodes, SortDescriptions = { new SortDescription("Name", ListSortDirection.Ascending)}}; + _contentView = _collectionViewSource.View; + _contentView.Filter += Filter; + } + + _contentList.ItemsSource = _contentView; + } + + private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) => (d as VisualScriptNodeCreationBox)?.ItemsSourceChanged(); + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs new file mode 100644 index 000000000..e79a195de --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs @@ -0,0 +1,137 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Artemis.VisualScripting.Editor.Controls.Wrapper; + +namespace Artemis.VisualScripting.Editor.Controls +{ + public class VisualScriptNodePresenter : Control + { + #region Properties & Fields + + private bool _isDragging; + private bool _isDragged; + private bool _isDeselected; + private Point _dragStartPosition; + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty NodeProperty = DependencyProperty.Register( + "Node", typeof(VisualScriptNode), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(VisualScriptNode))); + + public VisualScriptNode Node + { + get => (VisualScriptNode)GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly DependencyProperty TitleBrushProperty = DependencyProperty.Register( + "TitleBrush", typeof(Brush), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(Brush))); + + public Brush TitleBrush + { + get => (Brush)GetValue(TitleBrushProperty); + set => SetValue(TitleBrushProperty, value); + } + + #endregion + + #region Methods + + protected override Size MeasureOverride(Size constraint) + { + int SnapToGridSize(int value) + { + int mod = value % Node.Script.GridSize; + return mod switch + { + < 0 => Node.Script.GridSize, + > 0 => value + (Node.Script.GridSize - mod), + _ => value + }; + } + + Size neededSize = base.MeasureOverride(constraint); + int width = (int)Math.Ceiling(neededSize.Width); + int height = (int)Math.Ceiling(neededSize.Height); + + return new Size(SnapToGridSize(width), SnapToGridSize(height)); + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs args) + { + base.OnMouseLeftButtonDown(args); + + _isDragged = false; + _isDragging = true; + _isDeselected = false; + + bool isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + if (!Node.IsSelected) + Node.Select(isShiftDown); + else if (isShiftDown) + { + _isDeselected = true; + Node.Deselect(true); + } + + Node.DragStart(); + + _dragStartPosition = PointToScreen(args.GetPosition(this)); + + CaptureMouse(); + args.Handled = true; + } + + protected override void OnMouseLeftButtonUp(MouseButtonEventArgs args) + { + base.OnMouseLeftButtonUp(args); + + if (_isDragging) + { + _isDragging = false; + Node.DragEnd(); + + ReleaseMouseCapture(); + } + + if (!_isDragged && !_isDeselected) + Node.Select(Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)); + + args.Handled = true; + } + + protected override void OnMouseMove(MouseEventArgs args) + { + base.OnMouseMove(args); + + if (!_isDragging) return; + + Point mousePosition = PointToScreen(args.GetPosition(this)); + Vector offset = mousePosition - _dragStartPosition; + + if (args.LeftButton == MouseButtonState.Pressed) + { + _dragStartPosition = mousePosition; + Node.DragMove(offset.X, offset.Y); + + if ((offset.X != 0) && (offset.Y != 0)) + _isDragged = true; + } + else + { + _isDragging = false; + Node.DragEnd(); + ReleaseMouseCapture(); + } + + args.Handled = true; + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs new file mode 100644 index 000000000..75931bc08 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs @@ -0,0 +1,184 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Artemis.VisualScripting.Editor.Controls.Wrapper; +using Artemis.VisualScripting.Model; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_DOT, Type = typeof(FrameworkElement))] + public class VisualScriptPinPresenter : Control + { + #region Constants + + private const string PART_DOT = "PART_Dot"; + + #endregion + + #region Properties & Fields + + private VisualScriptNodePresenter _nodePresenter; + private FrameworkElement _dot; + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty PinProperty = DependencyProperty.Register( + "Pin", typeof(VisualScriptPin), typeof(VisualScriptPinPresenter), new PropertyMetadata(default(VisualScriptPin), PinChanged)); + + public VisualScriptPin Pin + { + get => (VisualScriptPin)GetValue(PinProperty); + set => SetValue(PinProperty, value); + } + + #endregion + + #region Constructors + + public VisualScriptPinPresenter() + { + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + #endregion + + #region Methods + + private void OnLoaded(object sender, RoutedEventArgs args) + { + DependencyObject parent = this; + while ((parent = VisualTreeHelper.GetParent(parent)) != null) + { + if (parent is VisualScriptNodePresenter nodePresenter) + { + _nodePresenter = nodePresenter; + break; + } + } + + LayoutUpdated += OnLayoutUpdated; + + UpdateAbsoluteLocation(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + LayoutUpdated -= OnLayoutUpdated; + _nodePresenter = null; + } + + private void OnNodePropertyChanged(object sender, PropertyChangedEventArgs e) + { + UpdateAbsoluteLocation(); + } + + private void OnLayoutUpdated(object sender, EventArgs args) + { + _dot = GetTemplateChild(PART_DOT) as FrameworkElement ?? throw new NullReferenceException($"The Element '{PART_DOT}' is missing."); + + _dot.AllowDrop = true; + + _dot.MouseDown += OnDotMouseDown; + _dot.MouseMove += OnDotMouseMove; + _dot.Drop += OnDotDrop; + _dot.DragEnter += OnDotDrag; + _dot.DragOver += OnDotDrag; + + UpdateAbsoluteLocation(); + } + + private static void PinChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPinPresenter presenter) return; + + presenter.PinChanged(args); + } + + private void PinChanged(DependencyPropertyChangedEventArgs args) + { + if (args.OldValue is VisualScriptPin oldPin) + oldPin.Node.Node.PropertyChanged -= OnNodePropertyChanged; + + if (args.NewValue is VisualScriptPin newPin) + newPin.Node.Node.PropertyChanged += OnNodePropertyChanged; + + UpdateAbsoluteLocation(); + } + + private void UpdateAbsoluteLocation() + { + if ((Pin == null) || (_nodePresenter == null)) return; + + try + { + double circleRadius = ActualHeight / 2.0; + double xOffset = Pin.Pin.Direction == PinDirection.Input ? circleRadius : ActualWidth - circleRadius; + Point relativePosition = this.TransformToVisual(_nodePresenter).Transform(new Point(xOffset, circleRadius)); + Pin.AbsolutePosition = new Point(Pin.Node.X + relativePosition.X, Pin.Node.Y + relativePosition.Y); + } + catch + { + Pin.AbsolutePosition = new Point(0, 0); + } + } + + private void OnDotMouseDown(object sender, MouseButtonEventArgs args) + { + if (args.ChangedButton == MouseButton.Middle) + Pin.DisconnectAll(); + + args.Handled = true; + } + + private void OnDotMouseMove(object sender, MouseEventArgs args) + { + if (args.LeftButton == MouseButtonState.Pressed) + { + Pin.SetConnecting(true); + DragDrop.DoDragDrop(this, Pin, DragDropEffects.Link); + Pin.SetConnecting(false); + + args.Handled = true; + } + } + + private void OnDotDrag(object sender, DragEventArgs args) + { + if (!args.Data.GetDataPresent(typeof(VisualScriptPin))) + args.Effects = DragDropEffects.None; + else + { + VisualScriptPin sourcePin = (VisualScriptPin)args.Data.GetData(typeof(VisualScriptPin)); + if (sourcePin == null) + args.Effects = DragDropEffects.None; + else + args.Effects = ((sourcePin.Pin.Direction != Pin.Pin.Direction) && (sourcePin.Pin.Node != Pin.Pin.Node) && IsTypeCompatible(sourcePin.Pin.Type)) ? DragDropEffects.Link : DragDropEffects.None; + } + + if (args.Effects == DragDropEffects.None) + args.Handled = true; + } + + private void OnDotDrop(object sender, DragEventArgs args) + { + if (!args.Data.GetDataPresent(typeof(VisualScriptPin))) return; + + VisualScriptPin sourcePin = (VisualScriptPin)args.Data.GetData(typeof(VisualScriptPin)); + if ((sourcePin == null) || !IsTypeCompatible(sourcePin.Pin.Type)) return; + + try { new VisualScriptCable(Pin, sourcePin); } catch { /**/ } + + args.Handled = true; + } + + private bool IsTypeCompatible(Type type) => ((Pin.Pin.Direction == PinDirection.Input) && (Pin.Pin.Type == typeof(object))) || (Pin.Pin.Type == type); + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs new file mode 100644 index 000000000..327c96be9 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Editor.Controls.Wrapper; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls +{ + [TemplatePart(Name = PART_CANVAS, Type = typeof(Canvas))] + [TemplatePart(Name = PART_NODELIST, Type = typeof(ItemsControl))] + [TemplatePart(Name = PART_CABLELIST, Type = typeof(ItemsControl))] + [TemplatePart(Name = PART_SELECTION_BORDER, Type = typeof(Border))] + [TemplatePart(Name = PART_CREATION_BOX_PARENT, Type = typeof(Panel))] + public class VisualScriptPresenter : Control + { + #region Constants + + private const string PART_CANVAS = "PART_Canvas"; + private const string PART_NODELIST = "PART_NodeList"; + private const string PART_CABLELIST = "PART_CableList"; + private const string PART_SELECTION_BORDER = "PART_SelectionBorder"; + private const string PART_CREATION_BOX_PARENT = "PART_CreationBoxParent"; + + #endregion + + #region Properties & Fields + + private Canvas _canvas; + private ItemsControl _nodeList; + private ItemsControl _cableList; + private Border _selectionBorder; + private TranslateTransform _canvasViewPortTransform; + private Panel _creationBoxParent; + + private Vector _viewportCenter = new(0, 0); + + private bool _dragCanvas = false; + private Point _dragCanvasStartLocation; + private Vector _dragCanvasStartOffset; + private bool _movedDuringDrag = false; + + private bool _boxSelect = false; + private Point _boxSelectStartPoint; + + private Point _lastRightClickLocation; + + internal VisualScript VisualScript { get; private set; } + + #endregion + + #region Dependency Properties + + public static readonly DependencyProperty ScriptProperty = DependencyProperty.Register( + "Script", typeof(IScript), typeof(VisualScriptPresenter), new PropertyMetadata(default(IScript), ScriptChanged)); + + public IScript Script + { + get => (IScript)GetValue(ScriptProperty); + set => SetValue(ScriptProperty, value); + } + + public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register( + "Scale", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(1.0, ScaleChanged)); + + public double Scale + { + get => (double)GetValue(ScaleProperty); + set => SetValue(ScaleProperty, value); + } + + public static readonly DependencyProperty MinScaleProperty = DependencyProperty.Register( + "MinScale", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(0.15)); + + public double MinScale + { + get => (double)GetValue(MinScaleProperty); + set => SetValue(MinScaleProperty, value); + } + + public static readonly DependencyProperty MaxScaleProperty = DependencyProperty.Register( + "MaxScale", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(1.0)); + + public double MaxScale + { + get => (double)GetValue(MaxScaleProperty); + set => SetValue(MaxScaleProperty, value); + } + + public static readonly DependencyProperty ScaleFactorProperty = DependencyProperty.Register( + "ScaleFactor", typeof(double), typeof(VisualScriptPresenter), new PropertyMetadata(1.3)); + + public double ScaleFactor + { + get => (double)GetValue(ScaleFactorProperty); + set => SetValue(ScaleFactorProperty, value); + } + + public static readonly DependencyProperty AvailableNodesProperty = DependencyProperty.Register( + "AvailableNodes", typeof(IEnumerable), typeof(VisualScriptPresenter), new PropertyMetadata(default(IEnumerable))); + + public IEnumerable AvailableNodes + { + get => (IEnumerable)GetValue(AvailableNodesProperty); + set => SetValue(AvailableNodesProperty, value); + } + + public static readonly DependencyProperty CreateNodeCommandProperty = DependencyProperty.Register( + "CreateNodeCommand", typeof(ICommand), typeof(VisualScriptPresenter), new PropertyMetadata(default(ICommand))); + + public ICommand CreateNodeCommand + { + get => (ICommand)GetValue(CreateNodeCommandProperty); + private set => SetValue(CreateNodeCommandProperty, value); + } + + public static readonly DependencyProperty GridSizeProperty = DependencyProperty.Register( + "GridSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(24)); + + public int GridSize + { + get => (int)GetValue(GridSizeProperty); + set => SetValue(GridSizeProperty, value); + } + + public static readonly DependencyProperty SurfaceSizeProperty = DependencyProperty.Register( + "SurfaceSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(16384)); + + public int SurfaceSize + { + get => (int)GetValue(SurfaceSizeProperty); + set => SetValue(SurfaceSizeProperty, value); + } + + #endregion + + #region Constructors + + public VisualScriptPresenter() + { + CreateNodeCommand = new ActionCommand(CreateNode); + + this.SizeChanged += OnSizeChanged; + } + + #endregion + + #region Methods + + public override void OnApplyTemplate() + { + _canvas = GetTemplateChild(PART_CANVAS) as Canvas ?? throw new NullReferenceException($"The Canvas '{PART_CANVAS}' is missing."); + _selectionBorder = GetTemplateChild(PART_SELECTION_BORDER) as Border ?? throw new NullReferenceException($"The Border '{PART_SELECTION_BORDER}' is missing."); + _nodeList = GetTemplateChild(PART_NODELIST) as ItemsControl ?? throw new NullReferenceException($"The ItemsControl '{PART_NODELIST}' is missing."); + _cableList = GetTemplateChild(PART_CABLELIST) as ItemsControl ?? throw new NullReferenceException($"The ItemsControl '{PART_CABLELIST}' is missing."); + _creationBoxParent = GetTemplateChild(PART_CREATION_BOX_PARENT) as Panel ?? throw new NullReferenceException($"The Panel '{PART_CREATION_BOX_PARENT}' is missing."); + + _canvas.AllowDrop = true; + + _canvas.RenderTransform = _canvasViewPortTransform = new TranslateTransform(0, 0); + _canvas.MouseLeftButtonDown += OnCanvasMouseLeftButtonDown; + _canvas.MouseLeftButtonUp += OnCanvasMouseLeftButtonUp; + _canvas.MouseRightButtonDown += OnCanvasMouseRightButtonDown; + _canvas.MouseRightButtonUp += OnCanvasMouseRightButtonUp; + _canvas.PreviewMouseRightButtonDown += OnCanvasPreviewMouseRightButtonDown; + _canvas.MouseMove += OnCanvasMouseMove; + _canvas.MouseWheel += OnCanvasMouseWheel; + _canvas.DragOver += OnCanvasDragOver; + + _nodeList.ItemsSource = VisualScript?.Nodes; + _cableList.ItemsSource = VisualScript?.Cables; + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs args) + { + if (sender is not VisualScriptPresenter scriptPresenter) return; + + scriptPresenter.UpdatePanning(); + } + + private static void ScriptChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPresenter scriptPresenter) return; + + scriptPresenter.ScriptChanged(args.NewValue is not Script script ? null : new VisualScript(script, scriptPresenter.SurfaceSize, scriptPresenter.GridSize)); + } + + private void ScriptChanged(VisualScript newScript) + { + if (VisualScript != null) + VisualScript.PropertyChanged -= OnVisualScriptPropertyChanged; + + VisualScript = newScript; + + if (VisualScript != null) + { + VisualScript.PropertyChanged += OnVisualScriptPropertyChanged; + + if (_nodeList != null) + _nodeList.ItemsSource = VisualScript?.Nodes; + + if (_cableList != null) + _cableList.ItemsSource = VisualScript?.Cables; + + VisualScript.Nodes.Clear(); + foreach (INode node in VisualScript.Script.Nodes) + InitializeNode(node); + } + + CenterAt(new Vector(0, 0)); + } + + private void OnVisualScriptPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == nameof(VisualScript.Cables)) + _cableList.ItemsSource = VisualScript.Cables; + } + + private void OnCanvasPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs args) + { + _lastRightClickLocation = args.GetPosition(_canvas); + } + + private void OnCanvasMouseLeftButtonDown(object sender, MouseButtonEventArgs args) + { + VisualScript.DeselectAllNodes(); + + _boxSelect = true; + _boxSelectStartPoint = args.GetPosition(_canvas); + Canvas.SetLeft(_selectionBorder, _boxSelectStartPoint.X); + Canvas.SetTop(_selectionBorder, _boxSelectStartPoint.Y); + _selectionBorder.Width = 0; + _selectionBorder.Height = 0; + _selectionBorder.Visibility = Visibility.Visible; + _canvas.CaptureMouse(); + + args.Handled = true; + } + + private void OnCanvasMouseLeftButtonUp(object sender, MouseButtonEventArgs args) + { + if (_boxSelect) + { + _boxSelect = false; + _canvas.ReleaseMouseCapture(); + Mouse.OverrideCursor = null; + _selectionBorder.Visibility = Visibility.Hidden; + SelectWithinRectangle(new Rect(Canvas.GetLeft(_selectionBorder), Canvas.GetTop(_selectionBorder), _selectionBorder.Width, _selectionBorder.Height)); + args.Handled = _movedDuringDrag; + } + } + + private void OnCanvasMouseRightButtonDown(object sender, MouseButtonEventArgs args) + { + _dragCanvas = true; + _dragCanvasStartLocation = args.GetPosition(this); + _dragCanvasStartOffset = _viewportCenter; + + _movedDuringDrag = false; + + _canvas.CaptureMouse(); + + args.Handled = true; + } + + private void OnCanvasMouseRightButtonUp(object sender, MouseButtonEventArgs args) + { + if (_dragCanvas) + { + _dragCanvas = false; + _canvas.ReleaseMouseCapture(); + Mouse.OverrideCursor = null; + args.Handled = _movedDuringDrag; + } + } + + private void OnCanvasMouseMove(object sender, MouseEventArgs args) + { + if (_dragCanvas) + { + if (args.RightButton == MouseButtonState.Pressed) + { + Vector newLocation = _dragCanvasStartOffset + (((args.GetPosition(this) - _dragCanvasStartLocation)) * (1.0 / Scale)); + CenterAt(newLocation); + + _movedDuringDrag = true; + Mouse.OverrideCursor = Cursors.ScrollAll; + } + else + _dragCanvas = false; + + args.Handled = true; + } + else if (_boxSelect) + { + if (args.LeftButton == MouseButtonState.Pressed) + { + double x = _boxSelectStartPoint.X; + double y = _boxSelectStartPoint.Y; + Point mousePosition = args.GetPosition(_canvas); + + double rectX = mousePosition.X > x ? x : mousePosition.X; + double rectY = mousePosition.Y > y ? y : mousePosition.Y; + double rectWidth = Math.Abs(x - mousePosition.X); + double rectHeight = Math.Abs(y - mousePosition.Y); + + Canvas.SetLeft(_selectionBorder, rectX); + Canvas.SetTop(_selectionBorder, rectY); + _selectionBorder.Width = rectWidth; + _selectionBorder.Height = rectHeight; + } + else + _boxSelect = false; + + args.Handled = true; + } + } + + private void OnCanvasDragOver(object sender, DragEventArgs args) + { + if (VisualScript == null) return; + + if (VisualScript.IsConnecting) + VisualScript.OnDragOver(args.GetPosition(_canvas)); + } + + private void OnCanvasMouseWheel(object sender, MouseWheelEventArgs args) + { + if (args.Delta < 0) + Scale /= ScaleFactor; + else + Scale *= ScaleFactor; + + Scale = Clamp(Scale, MinScale, MaxScale); + + _canvas.LayoutTransform = new ScaleTransform(Scale, Scale); + + UpdatePanning(); + + args.Handled = true; + } + + private void SelectWithinRectangle(Rect rectangle) + { + if (Script == null) return; + + VisualScript.DeselectAllNodes(); + + for (int i = 0; i < _nodeList.Items.Count; i++) + { + ContentPresenter nodeControl = (ContentPresenter)_nodeList.ItemContainerGenerator.ContainerFromIndex(i); + VisualScriptNode node = (VisualScriptNode)nodeControl.Content; + + double nodeWidth = nodeControl.ActualWidth; + double nodeHeight = nodeControl.ActualHeight; + + if (rectangle.IntersectsWith(new Rect(node.X, node.Y, nodeWidth, nodeHeight))) + node.Select(true); + } + } + + private static void ScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPresenter presenter) return; + if (presenter.VisualScript == null) return; + + presenter.VisualScript.NodeDragScale = 1.0 / presenter.Scale; + } + + private void CreateNode(NodeData nodeData) + { + if (nodeData == null) return; + + if (_creationBoxParent.ContextMenu != null) + _creationBoxParent.ContextMenu.IsOpen = false; + + INode node = nodeData.CreateNode(); + Script.AddNode(node); + + InitializeNode(node, _lastRightClickLocation); + } + + private void InitializeNode(INode node, Point? initialLocation = null) + { + VisualScriptNode visualScriptNode = new(VisualScript, node); + if (initialLocation != null) + { + visualScriptNode.X = initialLocation.Value.X; + visualScriptNode.Y = initialLocation.Value.Y; + } + visualScriptNode.SnapNodeToGrid(); + VisualScript.Nodes.Add(visualScriptNode); + } + + private void CenterAt(Vector vector) + { + double halfSurface = (SurfaceSize / 2.0); + _viewportCenter = Clamp(vector, new Vector(-halfSurface, -halfSurface), new Vector(halfSurface, halfSurface)); + UpdatePanning(); + } + + private void UpdatePanning() + { + if (_canvasViewPortTransform == null) return; + + double surfaceOffset = (SurfaceSize / 2.0) * Scale; + _canvasViewPortTransform.X = (((_viewportCenter.X * Scale) + (ActualWidth / 2.0))) - surfaceOffset; + _canvasViewPortTransform.Y = (((_viewportCenter.Y * Scale) + (ActualHeight / 2.0))) - surfaceOffset; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double Clamp(double value, double min, double max) + { + // ReSharper disable ConvertIfStatementToReturnStatement - I'm not sure why, but inlining this statement reduces performance by ~10% + if (value < min) return min; + if (value > max) return max; + return value; + // ReSharper restore ConvertIfStatementToReturnStatement + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector Clamp(Vector value, Vector min, Vector max) + { + double x = Clamp(value.X, min.X, max.X); + double y = Clamp(value.Y, min.Y, max.Y); + return new Vector(x, y); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs new file mode 100644 index 000000000..57fdaaa5b --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Windows; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Events; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScript : AbstractBindable + { + #region Properties & Fields + + private readonly HashSet _selectedNodes = new(); + private readonly Dictionary _nodeStartPositions = new(); + private double _nodeDragAccumulationX; + private double _nodeDragAccumulationY; + + public IScript Script { get; } + + public int SurfaceSize { get; } + public int GridSize { get; } + + private double _nodeDragScale = 1; + public double NodeDragScale + { + get => _nodeDragScale; + set => SetProperty(ref _nodeDragScale, value); + } + + private VisualScriptPin _isConnectingPin; + private VisualScriptPin IsConnectingPin + { + get => _isConnectingPin; + set + { + if (SetProperty(ref _isConnectingPin, value)) + OnPropertyChanged(nameof(IsConnecting)); + } + } + + public ObservableCollection Nodes { get; } = new(); + + public IEnumerable Cables => Nodes.SelectMany(n => n.InputPins.SelectMany(p => p.Connections)) + .Concat(Nodes.SelectMany(n => n.OutputPins.SelectMany(p => p.Connections))) + .Concat(Nodes.SelectMany(n => n.InputPinCollections.SelectMany(p => p.Pins).SelectMany(p => p.Connections))) + .Concat(Nodes.SelectMany(n => n.OutputPinCollections.SelectMany(p => p.Pins).SelectMany(p => p.Connections))) + .Distinct(); + + public bool IsConnecting => IsConnectingPin != null; + + #endregion + + #region Constructors + + public VisualScript(IScript script, int surfaceSize, int gridSize) + { + this.Script = script; + this.SurfaceSize = surfaceSize; + this.GridSize = gridSize; + + Nodes.CollectionChanged += OnNodeCollectionChanged; + } + + #endregion + + #region Methods + + public void DeselectAllNodes(VisualScriptNode except = null) + { + List selectedNodes = _selectedNodes.ToList(); + foreach (VisualScriptNode node in selectedNodes.Where(n => n != except)) + node.Deselect(); + } + + private void OnNodeCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (args.OldItems != null) + UnregisterNodes(args.OldItems.Cast()); + + if (args.NewItems != null) + RegisterNodes(args.NewItems.Cast()); + } + + private void RegisterNodes(IEnumerable nodes) + { + foreach (VisualScriptNode node in nodes) + { + node.IsSelectedChanged += OnNodeIsSelectedChanged; + node.DragStarting += OnNodeDragStarting; + node.DragEnding += OnNodeDragEnding; + node.DragMoving += OnNodeDragMoving; + + if (node.IsSelected) + _selectedNodes.Add(node); + } + } + + private void UnregisterNodes(IEnumerable nodes) + { + foreach (VisualScriptNode node in nodes) + { + node.IsSelectedChanged -= OnNodeIsSelectedChanged; + node.DragStarting -= OnNodeDragStarting; + node.DragEnding -= OnNodeDragEnding; + node.DragMoving -= OnNodeDragMoving; + + _selectedNodes.Remove(node); + } + } + + private void OnNodeIsSelectedChanged(object sender, VisualScriptNodeIsSelectedChangedEventArgs args) + { + if (sender is not VisualScriptNode node) return; + + if (args.IsSelected) + { + if (!args.AlterSelection) + DeselectAllNodes(node); + + _selectedNodes.Add(node); + } + else + _selectedNodes.Remove(node); + } + + private void OnNodeDragStarting(object sender, EventArgs args) + { + _nodeDragAccumulationX = 0; + _nodeDragAccumulationY = 0; + _nodeStartPositions.Clear(); + + foreach (VisualScriptNode node in _selectedNodes) + _nodeStartPositions.Add(node, (node.X, node.Y)); + } + + private void OnNodeDragEnding(object sender, EventArgs args) + { + foreach (VisualScriptNode node in _selectedNodes) + node.SnapNodeToGrid(); + } + + private void OnNodeDragMoving(object sender, VisualScriptNodeDragMovingEventArgs args) + { + _nodeDragAccumulationX += args.DX * NodeDragScale; + _nodeDragAccumulationY += args.DY * NodeDragScale; + + foreach (VisualScriptNode node in _selectedNodes) + { + node.X = _nodeStartPositions[node].X + _nodeDragAccumulationX; + node.Y = _nodeStartPositions[node].Y + _nodeDragAccumulationY; + node.SnapNodeToGrid(); + } + } + + internal void OnDragOver(Point position) + { + if (IsConnectingPin == null) return; + + IsConnectingPin.AbsolutePosition = position; + } + + internal void OnIsConnectingPinChanged(VisualScriptPin isConnectingPin) + { + IsConnectingPin = isConnectingPin; + } + + internal void OnPinConnected(PinConnectedEventArgs args) + { + OnPropertyChanged(nameof(Cables)); + } + + internal void OnPinDisconnected(PinDisconnectedEventArgs args) + { + OnPropertyChanged(nameof(Cables)); + } + + public void RemoveNode(VisualScriptNode node) + { + Nodes.Remove(node); + Script.RemoveNode(node.Node); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs new file mode 100644 index 000000000..9fe3943ae --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptCable.cs @@ -0,0 +1,67 @@ +using System; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptCable : AbstractBindable + { + #region Properties & Fields + + private VisualScriptPin _from; + public VisualScriptPin From + { + get => _from; + private set => SetProperty(ref _from, value); + } + + private VisualScriptPin _to; + public VisualScriptPin To + { + get => _to; + private set => SetProperty(ref _to, value); + } + + #endregion + + #region Constructors + + public VisualScriptCable(VisualScriptPin pin1, VisualScriptPin pin2) + { + if ((pin1.Pin.Direction == PinDirection.Input) && (pin2.Pin.Direction == PinDirection.Input)) + throw new ArgumentException("Can't connect two input pins."); + + if ((pin1.Pin.Direction == PinDirection.Output) && (pin2.Pin.Direction == PinDirection.Output)) + throw new ArgumentException("Can't connect two output pins."); + + From = pin1.Pin.Direction == PinDirection.Output ? pin1 : pin2; + To = pin1.Pin.Direction == PinDirection.Input ? pin1 : pin2; + + From.ConnectTo(this); + To.ConnectTo(this); + } + + #endregion + + #region Methods + + internal void Disconnect() + { + From?.Disconnect(this); + To?.Disconnect(this); + + From = null; + To = null; + } + + internal IPin GetConnectedPin(IPin pin) + { + if (From.Pin == pin) return To.Pin; + if (To.Pin == pin) return From.Pin; + return null; + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs new file mode 100644 index 000000000..40bf11d6c --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.ObjectModel; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Events; +using Artemis.VisualScripting.Internal; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptNode : AbstractBindable + { + #region Properties & Fields + + private double _locationOffset; + + public VisualScript Script { get; } + public INode Node { get; } + + private bool _isSelected; + public bool IsSelected + { + get => _isSelected; + private set => SetProperty(ref _isSelected, value); + } + + private ObservableCollection _inputPins = new(); + public ObservableCollection InputPins + { + get => _inputPins; + private set => SetProperty(ref _inputPins, value); + } + + private ObservableCollection _outputPins = new(); + public ObservableCollection OutputPins + { + get => _outputPins; + private set => SetProperty(ref _outputPins, value); + } + + private ObservableCollection _inputPinCollections = new(); + public ObservableCollection InputPinCollections + { + get => _inputPinCollections; + private set => SetProperty(ref _inputPinCollections, value); + } + + private ObservableCollection _outputPinCollections = new(); + public ObservableCollection OutputPinCollections + { + get => _outputPinCollections; + private set => SetProperty(ref _outputPinCollections, value); + } + + public double X + { + get => Node.X + _locationOffset; + set + { + Node.X = value - _locationOffset; + OnPropertyChanged(); + } + } + + public double Y + { + get => Node.Y + _locationOffset; + set + { + Node.Y = value - _locationOffset; + OnPropertyChanged(); + } + } + + #endregion + + #region Commands + + private ActionCommand _removeCommand; + public ActionCommand RemoveCommand => _removeCommand ??= new ActionCommand(Remove, RemoveCanExecute); + + #endregion + + #region Events + + public event EventHandler IsSelectedChanged; + public event EventHandler DragStarting; + public event EventHandler DragEnding; + public event EventHandler DragMoving; + + #endregion + + #region Constructors + + public VisualScriptNode(VisualScript script, INode node) + { + this.Script = script; + this.Node = node; + + _locationOffset = script.SurfaceSize / 2.0; + + foreach (IPin pin in node.Pins) + { + if (pin.Direction == PinDirection.Input) + InputPins.Add(new VisualScriptPin(this, pin)); + else + OutputPins.Add(new VisualScriptPin(this, pin)); + } + + foreach (IPinCollection pinCollection in node.PinCollections) + { + if (pinCollection.Direction == PinDirection.Input) + InputPinCollections.Add(new VisualScriptPinCollection(this, pinCollection)); + else + OutputPinCollections.Add(new VisualScriptPinCollection(this, pinCollection)); + } + } + + #endregion + + #region Methods + + public void SnapNodeToGrid() + { + X -= X % Script.GridSize; + Y -= Y % Script.GridSize; + } + + public void Select(bool alterSelection = false) + { + IsSelected = true; + OnIsSelectedChanged(IsSelected, alterSelection); + } + + public void Deselect(bool alterSelection = false) + { + IsSelected = false; + OnIsSelectedChanged(IsSelected, alterSelection); + } + + public void DragStart() => DragStarting?.Invoke(this, new EventArgs()); + public void DragEnd() => DragEnding?.Invoke(this, new EventArgs()); + public void DragMove(double dx, double dy) => DragMoving?.Invoke(this, new VisualScriptNodeDragMovingEventArgs(dx, dy)); + + private void OnIsSelectedChanged(bool isSelected, bool alterSelection) => IsSelectedChanged?.Invoke(this, new VisualScriptNodeIsSelectedChangedEventArgs(isSelected, alterSelection)); + + internal void OnPinConnected(PinConnectedEventArgs args) => Script.OnPinConnected(args); + internal void OnPinDisconnected(PinDisconnectedEventArgs args) => Script.OnPinDisconnected(args); + + internal void OnIsConnectingPinChanged(VisualScriptPin isConnectingPin) => Script.OnIsConnectingPinChanged(isConnectingPin); + + private void DisconnectAllPins() + { + foreach (VisualScriptPin pin in InputPins) + pin.DisconnectAll(); + + foreach (VisualScriptPin pin in OutputPins) + pin.DisconnectAll(); + + foreach (VisualScriptPinCollection pinCollection in InputPinCollections) + foreach (VisualScriptPin pin in pinCollection.Pins) + pin.DisconnectAll(); + + foreach (VisualScriptPinCollection pinCollection in OutputPinCollections) + foreach (VisualScriptPin pin in pinCollection.Pins) + pin.DisconnectAll(); + } + + private void Remove() + { + DisconnectAllPins(); + Script.RemoveNode(this); + } + + private bool RemoveCanExecute() => Node is not IExitNode; + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs new file mode 100644 index 000000000..00b6e5184 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.Events; +using Artemis.VisualScripting.Internal; +using Artemis.VisualScripting.Model; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptPin : AbstractBindable + { + #region Constants + + private const double CABLE_OFFSET = 24 * 4; + + #endregion + + #region Properties & Fields + + private VisualScriptPin _isConnectingPin; + private VisualScriptCable _isConnectingCable; + + public VisualScriptNode Node { get; } + public IPin Pin { get; } + + public ObservableCollection Connections { get; } = new(); + + private Point _absolutePosition; + public Point AbsolutePosition + { + get => _absolutePosition; + internal set + { + if (SetProperty(ref _absolutePosition, value)) + OnPropertyChanged(nameof(AbsoluteCableTargetPosition)); + } + } + + public Point AbsoluteCableTargetPosition => Pin.Direction == PinDirection.Input ? new Point(AbsolutePosition.X - CABLE_OFFSET, AbsolutePosition.Y) : new Point(AbsolutePosition.X + CABLE_OFFSET, AbsolutePosition.Y); + + #endregion + + #region Constructors + + public VisualScriptPin(VisualScriptNode node, IPin pin) + { + this.Node = node; + this.Pin = pin; + } + + #endregion + + #region Methods + + public void SetConnecting(bool isConnecting) + { + if (isConnecting) + { + if (_isConnectingCable != null) + SetConnecting(false); + + _isConnectingPin = new VisualScriptPin(null, new IsConnectingPin(Pin.Direction == PinDirection.Input ? PinDirection.Output : PinDirection.Input)) { AbsolutePosition = AbsolutePosition }; + _isConnectingCable = new VisualScriptCable(this, _isConnectingPin); + Node.OnIsConnectingPinChanged(_isConnectingPin); + } + else + { + _isConnectingCable.Disconnect(); + _isConnectingCable = null; + _isConnectingPin = null; + Node.OnIsConnectingPinChanged(_isConnectingPin); + } + } + + internal void ConnectTo(VisualScriptCable cable) + { + if (Connections.Contains(cable)) return; + + if (Pin.Direction == PinDirection.Input) + { + List cables = Connections.ToList(); + foreach (VisualScriptCable c in cables) + c.Disconnect(); + } + + Connections.Add(cable); + Pin.ConnectTo(cable.GetConnectedPin(Pin)); + + Node?.OnPinConnected(new PinConnectedEventArgs(this, cable)); + } + + public void DisconnectAll() + { + List cables = Connections.ToList(); + foreach (VisualScriptCable cable in cables) + cable.Disconnect(); + } + + internal void Disconnect(VisualScriptCable cable) + { + Connections.Remove(cable); + Pin.DisconnectFrom(cable.GetConnectedPin(Pin)); + + Node?.OnPinDisconnected(new PinDisconnectedEventArgs(this, cable)); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs new file mode 100644 index 000000000..3e20fe8c5 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs @@ -0,0 +1,58 @@ +using System.Collections.ObjectModel; +using Artemis.Core.VisualScripting; +using Artemis.VisualScripting.ViewModel; + +namespace Artemis.VisualScripting.Editor.Controls.Wrapper +{ + public class VisualScriptPinCollection : AbstractBindable + { + #region Properties & Fields + + public VisualScriptNode Node { get; } + public IPinCollection PinCollection { get; } + + public ObservableCollection Pins { get; } = new(); + + #endregion + + #region Commands + + private ActionCommand _addPinCommand; + public ActionCommand AddPinCommand => _addPinCommand ??= new ActionCommand(AddPin); + + private ActionCommand _removePinCommand; + public ActionCommand RemovePinCommand => _removePinCommand ??= new ActionCommand(RemovePin); + + #endregion + + #region Constructors + + public VisualScriptPinCollection(VisualScriptNode node, IPinCollection pinCollection) + { + this.Node = node; + this.PinCollection = pinCollection; + + foreach (IPin pin in PinCollection) + Pins.Add(new VisualScriptPin(node, pin)); + } + + #endregion + + #region Methods + + public void AddPin() + { + IPin pin = PinCollection.AddPin(); + Pins.Add(new VisualScriptPin(Node, pin)); + } + + public void RemovePin(VisualScriptPin pin) + { + pin.DisconnectAll(); + PinCollection.Remove(pin.Pin); + Pins.Remove(pin); + } + + #endregion + } +} diff --git a/src/Artemis.VisualScripting/Editor/EditorStyles.xaml b/src/Artemis.VisualScripting/Editor/EditorStyles.xaml new file mode 100644 index 000000000..743c4dead --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/EditorStyles.xaml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml new file mode 100644 index 000000000..d51f0cfa3 --- /dev/null +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DataModelPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/DataModelPicker.xaml.cs new file mode 100644 index 000000000..036031368 --- /dev/null +++ b/src/Artemis.UI.Shared/Controls/DataModelPicker.xaml.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.UI.Shared.Services; +using Stylet; + +namespace Artemis.UI.Shared.Controls +{ + /// + /// Interaction logic for DataModelPicker.xaml + /// + public partial class DataModelPicker : INotifyPropertyChanged + { + private static IDataModelUIService _dataModelUIService; + + /// + /// Gets or sets data model path + /// + public static readonly DependencyProperty DataModelPathProperty = DependencyProperty.Register( + nameof(DataModelPath), typeof(DataModelPath), typeof(DataModelPicker), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, DataModelPathPropertyChangedCallback) + ); + + /// + /// Gets or sets the brush to use when drawing the button + /// + public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register( + nameof(Placeholder), typeof(string), typeof(DataModelPicker), + new FrameworkPropertyMetadata("Click to select") + ); + + /// + /// Gets or sets the brush to use when drawing the button + /// + public static readonly DependencyProperty ShowDataModelValuesProperty = DependencyProperty.Register(nameof(ShowDataModelValues), typeof(bool), typeof(DataModelPicker)); + + /// + /// Gets or sets the brush to use when drawing the button + /// + public static readonly DependencyProperty ShowFullPathProperty = DependencyProperty.Register(nameof(ShowFullPath), typeof(bool), typeof(DataModelPicker)); + + /// + /// Gets or sets the brush to use when drawing the button + /// + public static readonly DependencyProperty ButtonBrushProperty = DependencyProperty.Register(nameof(ButtonBrush), typeof(Brush), typeof(DataModelPicker)); + + /// + /// A list of extra modules to show data models of + /// + public static readonly DependencyProperty ModulesProperty = DependencyProperty.Register( + nameof(Modules), typeof(BindableCollection), typeof(DataModelPicker), + new FrameworkPropertyMetadata(new BindableCollection(), FrameworkPropertyMetadataOptions.None, ModulesPropertyChangedCallback) + ); + + /// + /// The data model view model to show, if not provided one will be retrieved by the control + /// + public static readonly DependencyProperty DataModelViewModelProperty = DependencyProperty.Register( + nameof(DataModelViewModel), typeof(DataModelPropertiesViewModel), typeof(DataModelPicker), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, DataModelViewModelPropertyChangedCallback) + ); + + /// + /// A list of data model view models to show + /// + public static readonly DependencyProperty ExtraDataModelViewModelsProperty = DependencyProperty.Register( + nameof(ExtraDataModelViewModels), typeof(BindableCollection), typeof(DataModelPicker), + new FrameworkPropertyMetadata(new BindableCollection(), FrameworkPropertyMetadataOptions.None, ExtraDataModelViewModelsPropertyChangedCallback) + ); + + /// + /// A list of data model view models to show + /// + public static readonly DependencyProperty FilterTypesProperty = DependencyProperty.Register( + nameof(FilterTypes), typeof(BindableCollection), typeof(DataModelPicker), + new FrameworkPropertyMetadata(new BindableCollection()) + ); + + public DataModelPicker() + { + SelectPropertyCommand = new DelegateCommand(ExecuteSelectPropertyCommand); + + InitializeComponent(); + GetDataModel(); + UpdateValueDisplay(); + } + + /// + /// Gets or sets the brush to use when drawing the button + /// + public DataModelPath? DataModelPath + { + get => (DataModelPath) GetValue(DataModelPathProperty); + set => SetValue(DataModelPathProperty, value); + } + + /// + /// Gets or sets the brush to use when drawing the button + /// + public string Placeholder + { + get => (string) GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + /// + /// Gets or sets the brush to use when drawing the button + /// + public bool ShowDataModelValues + { + get => (bool) GetValue(ShowDataModelValuesProperty); + set => SetValue(ShowDataModelValuesProperty, value); + } + + /// + /// Gets or sets the brush to use when drawing the button + /// + public bool ShowFullPath + { + get => (bool) GetValue(ShowFullPathProperty); + set => SetValue(ShowFullPathProperty, value); + } + + /// + /// Gets or sets the brush to use when drawing the button + /// + public Brush ButtonBrush + { + get => (Brush) GetValue(ButtonBrushProperty); + set => SetValue(ButtonBrushProperty, value); + } + + /// + /// Gets or sets a list of extra modules to show data models of + /// + public BindableCollection? Modules + { + get => (BindableCollection) GetValue(ModulesProperty); + set => SetValue(ModulesProperty, value); + } + + /// + /// Gets or sets the data model view model to show + /// + public DataModelPropertiesViewModel? DataModelViewModel + { + get => (DataModelPropertiesViewModel) GetValue(DataModelViewModelProperty); + set => SetValue(DataModelViewModelProperty, value); + } + + /// + /// Gets or sets a list of data model view models to show + /// + public BindableCollection? ExtraDataModelViewModels + { + get => (BindableCollection) GetValue(ExtraDataModelViewModelsProperty); + set => SetValue(ExtraDataModelViewModelsProperty, value); + } + + /// + /// Gets or sets the types of properties this view model will allow to be selected + /// + public BindableCollection? FilterTypes + { + get => (BindableCollection) GetValue(FilterTypesProperty); + set => SetValue(FilterTypesProperty, value); + } + + /// + /// Command used by view + /// + public DelegateCommand SelectPropertyCommand { get; } + + internal static IDataModelUIService DataModelUIService + { + set + { + if (_dataModelUIService != null) + throw new AccessViolationException("This is not for you to touch"); + _dataModelUIService = value; + } + } + + /// + /// Occurs when a new path has been selected + /// + public event EventHandler? DataModelPathSelected; + + /// + /// Invokes the event + /// + /// + protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e) + { + DataModelPathSelected?.Invoke(this, e); + } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private void GetDataModel() + { + ChangeDataModel(_dataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List(), true)); + } + + private void ChangeDataModel(DataModelPropertiesViewModel? dataModel) + { + if (DataModelViewModel != null) + { + DataModelViewModel.Dispose(); + DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested; + } + + DataModelViewModel = dataModel; + if (DataModelViewModel != null) + DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; + } + + private void UpdateValueDisplay() + { + ValueDisplay.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Visible : Visibility.Collapsed; + ValuePlaceholder.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Collapsed : Visibility.Visible; + + string? formattedPath = null; + if (DataModelPath != null && DataModelPath.IsValid) + formattedPath = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name)); + + DataModelButton.ToolTip = formattedPath; + ValueDisplayTextBlock.Text = ShowFullPath + ? formattedPath + : DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier; + } + + private void DataModelOnUpdateRequested(object? sender, EventArgs e) + { + DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); + if (ExtraDataModelViewModels == null) return; + foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) + extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); + } + + private void ExecuteSelectPropertyCommand(object? context) + { + if (context is not DataModelVisualizationViewModel selected) + return; + + DataModelPath = selected.DataModelPath; + OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath)); + } + + private void PropertyButton_OnClick(object sender, RoutedEventArgs e) + { + // DataContext is not set when using left button, I don't know why but there it is + if (sender is Button button && button.ContextMenu != null) + { + button.ContextMenu.DataContext = button.DataContext; + button.ContextMenu.IsOpen = true; + } + } + + private static void DataModelPathPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not DataModelPicker dataModelPicker) + return; + + if (e.OldValue is DataModelPath oldPath) + oldPath.Dispose(); + + dataModelPicker.UpdateValueDisplay(); + } + + private static void ModulesPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not DataModelPicker dataModelPicker) + return; + + dataModelPicker.GetDataModel(); + } + + private static void DataModelViewModelPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not DataModelPicker dataModelPicker) + return; + } + + private static void ExtraDataModelViewModelsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not DataModelPicker dataModelPicker) + return; + } + + public event PropertyChangedEventHandler? PropertyChanged; + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/IntToVisibilityConverter.cs b/src/Artemis.UI.Shared/Converters/IntToVisibilityConverter.cs new file mode 100644 index 000000000..2db2cd50a --- /dev/null +++ b/src/Artemis.UI.Shared/Converters/IntToVisibilityConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Artemis.UI.Shared +{ + /// + /// Converts to + /// + public class IntToVisibilityConverter : IValueConverter + { + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + Parameters direction; + if (parameter == null) + direction = Parameters.Normal; + else + direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter); + + return direction == Parameters.Normal + ? value is > 1 ? Visibility.Visible : Visibility.Collapsed + : value is > 1 ? Visibility.Collapsed : Visibility.Visible; + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + private enum Parameters + { + Normal, + Inverted + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml deleted file mode 100644 index 6ebda995f..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml.cs deleted file mode 100644 index a487bdfea..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicView.xaml.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Shared.Input -{ - /// - /// Interaction logic for DataModelDynamicView.xaml - /// - public partial class DataModelDynamicView : UserControl - { - /// - /// Creates a new instance of the class - /// - public DataModelDynamicView() - { - InitializeComponent(); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs deleted file mode 100644 index ad9b65584..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelDynamicViewModel.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Timers; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Shared.Services; -using MaterialDesignColors.ColorManipulation; -using Stylet; - -namespace Artemis.UI.Shared.Input -{ - /// - /// Represents a view model that allows selecting a data model property used by boolean operations on a certain data - /// model property - /// - public class DataModelDynamicViewModel : PropertyChangedBase, IDisposable - { - private readonly List _modules; - private readonly IDataModelUIService _dataModelUIService; - private readonly Timer _updateTimer; - private SolidColorBrush _buttonBrush = new(Color.FromRgb(171, 71, 188)); - private DataModelPath? _dataModelPath; - private DataModelPropertiesViewModel? _dataModelViewModel; - private bool _displaySwitchButton; - private Type[] _filterTypes = Array.Empty(); - private bool _isDataModelViewModelOpen; - private bool _isEnabled = true; - private string _placeholder = "Select a property"; - private readonly PluginSetting _showFullPath; - - internal DataModelDynamicViewModel(List modules, ISettingsService settingsService, IDataModelUIService dataModelUIService) - { - _modules = modules; - _dataModelUIService = dataModelUIService; - _updateTimer = new Timer(500); - _showFullPath = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); - - ExtraDataModelViewModels = new BindableCollection(); - ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues"); - SelectPropertyCommand = new DelegateCommand(ExecuteSelectPropertyCommand); - - Initialize(); - } - - /// - /// Gets or sets the brush to use for the input button - /// - public SolidColorBrush ButtonBrush - { - get => _buttonBrush; - set - { - if (!SetAndNotify(ref _buttonBrush, value)) return; - NotifyOfPropertyChange(nameof(SwitchButtonBrush)); - } - } - - /// - /// Gets the brush to use for the switch button - /// - public SolidColorBrush SwitchButtonBrush => new(ButtonBrush.Color.Darken()); - - /// - /// Gets or sets the placeholder text when no value is entered - /// - public string Placeholder - { - get => _placeholder; - set => SetAndNotify(ref _placeholder, value); - } - - /// - /// Gets or sets the enabled state of the input - /// - public bool IsEnabled - { - get => _isEnabled; - set => SetAndNotify(ref _isEnabled, value); - } - - /// - /// Gets or sets whether the switch button should be displayed - /// - public bool DisplaySwitchButton - { - get => _displaySwitchButton; - set => SetAndNotify(ref _displaySwitchButton, value); - } - - /// - /// Gets or sets the types of properties this view model will allow to be selected - /// - public Type[] FilterTypes - { - get => _filterTypes; - set - { - if (!SetAndNotify(ref _filterTypes, value)) return; - DataModelViewModel?.ApplyTypeFilter(true, FilterTypes); - } - } - - /// - /// Gets a bindable collection of extra data model view models to show - /// - public BindableCollection ExtraDataModelViewModels { get; } - - /// - /// Gets a boolean indicating whether there are any modules providing data models - /// - public bool HasNoModules => (DataModelViewModel == null || !DataModelViewModel.Children.Any()) && !HasExtraDataModels; - - /// - /// Gets a boolean indicating whether there are any extra data models - /// - public bool HasExtraDataModels => ExtraDataModelViewModels.Any(); - - /// - /// Command used by view - /// - public DelegateCommand SelectPropertyCommand { get; } - - /// - /// Setting used by view - /// - public PluginSetting ShowDataModelValues { get; } - - public PluginSetting ShowFullPath { get; } - - /// - /// Gets or sets root the data model view model - /// - public DataModelPropertiesViewModel? DataModelViewModel - { - get => _dataModelViewModel; - private set => SetAndNotify(ref _dataModelViewModel, value); - } - - /// - /// Gets or sets a boolean indicating whether the data model is open - /// - public bool IsDataModelViewModelOpen - { - get => _isDataModelViewModelOpen; - set - { - if (!SetAndNotify(ref _isDataModelViewModelOpen, value)) return; - if (value) - { - UpdateDataModelVisualization(); - if (DataModelViewModel != null) - OpenSelectedValue(DataModelViewModel); - } - } - } - - /// - /// Gets or sets the data model path of the currently selected data model property - /// - public DataModelPath? DataModelPath - { - get => _dataModelPath; - private set - { - if (!SetAndNotify(ref _dataModelPath, value)) return; - NotifyOfPropertyChange(nameof(IsValid)); - NotifyOfPropertyChange(nameof(DisplayValue)); - NotifyOfPropertyChange(nameof(DisplayPath)); - } - } - - /// - /// Gets a boolean indicating whether the current selection is valid - /// - public bool IsValid => DataModelPath?.IsValid ?? true; - - /// - /// Gets the display name of the currently selected property - /// - public string? DisplayValue - { - get - { - if (_showFullPath.Value) - return DisplayPath; - return DataModelPath?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier; - } - } - - /// - /// Gets the human readable path of the currently selected property - /// - public string DisplayPath - { - get - { - if (DataModelPath == null) - return "Click to select a property"; - if (!DataModelPath.IsValid) - return "Invalid path"; - - return string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription()!= null).Select(s => s.GetPropertyDescription()!.Name)); - } - } - - /// - /// Gets or sets a boolean indicating whether event child VMs should be loaded, defaults to - /// - public bool LoadEventChildren { get; set; } = true; - - /// - /// Changes the root data model VM stored in to the provided - /// - /// - /// The data model VM to set the new root data model to - public void ChangeDataModel(DataModelPropertiesViewModel dataModel) - { - if (DataModelViewModel != null) - DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested; - - DataModelViewModel = dataModel; - - if (DataModelViewModel != null) - DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; - } - - /// - /// Changes the currently selected property by its path - /// - /// The path of the property to set the selection to - public void ChangeDataModelPath(DataModelPath? dataModelPath) - { - DataModelPath?.Dispose(); - DataModelPath = dataModelPath != null ? new DataModelPath(dataModelPath) : null; - } - - /// - /// Requests switching the input type to static using a - /// - public void SwitchToStatic() - { - ChangeDataModelPath(null); - OnPropertySelected(new DataModelInputDynamicEventArgs(null)); - OnSwitchToStaticRequested(); - } - - private void Initialize() - { - // Get the data models - DataModelViewModel = _dataModelUIService.GetPluginDataModelVisualization(_modules, true); - if (DataModelViewModel != null) - DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; - ExtraDataModelViewModels.CollectionChanged += ExtraDataModelViewModelsOnCollectionChanged; - _updateTimer.Start(); - _updateTimer.Elapsed += OnUpdateTimerOnElapsed; - _showFullPath.SettingChanged += ShowFullPathOnSettingChanged; - } - - - private void ExecuteSelectPropertyCommand(object? context) - { - if (context is not DataModelVisualizationViewModel selected) - return; - - ChangeDataModelPath(selected.DataModelPath); - OnPropertySelected(new DataModelInputDynamicEventArgs(DataModelPath)); - } - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _updateTimer.Stop(); - _updateTimer.Dispose(); - _updateTimer.Elapsed -= OnUpdateTimerOnElapsed; - _showFullPath.SettingChanged -= ShowFullPathOnSettingChanged; - - DataModelViewModel?.Dispose(); - DataModelPath?.Dispose(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - #region Event handlers - - private void DataModelOnUpdateRequested(object? sender, EventArgs e) - { - DataModelViewModel?.ApplyTypeFilter(true, FilterTypes); - foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) - extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes); - } - - private void ExtraDataModelViewModelsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - NotifyOfPropertyChange(nameof(HasExtraDataModels)); - } - - private void OnUpdateTimerOnElapsed(object? sender, ElapsedEventArgs e) - { - if (!IsDataModelViewModelOpen) - return; - - UpdateDataModelVisualization(); - } - - private void UpdateDataModelVisualization() - { - DataModelViewModel?.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren)); - foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) - extraDataModelViewModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(LoadEventChildren)); - } - - private void OpenSelectedValue(DataModelVisualizationViewModel dataModelPropertiesViewModel) - { - if (DataModelPath == null) - return; - - if (dataModelPropertiesViewModel.Children.Any(c => c.DataModelPath != null && DataModelPath.Path.StartsWith(c.DataModelPath.Path))) - { - dataModelPropertiesViewModel.IsVisualizationExpanded = true; - foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in dataModelPropertiesViewModel.Children) - { - OpenSelectedValue(dataModelVisualizationViewModel); - } - } - } - - private void ShowFullPathOnSettingChanged(object? sender, EventArgs e) - { - NotifyOfPropertyChange(nameof(DisplayValue)); - } - - #endregion - - #region Events - - /// - /// Occurs when anew property has been selected - /// - public event EventHandler? PropertySelected; - - /// - /// Occurs when a switch to static input has been requested - /// - public event EventHandler? SwitchToStaticRequested; - - /// - /// Invokes the event - /// - /// - protected virtual void OnPropertySelected(DataModelInputDynamicEventArgs e) - { - PropertySelected?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnSwitchToStaticRequested() - { - SwitchToStaticRequested?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml deleted file mode 100644 index a069d383a..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticView.xaml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticViewModel.cs deleted file mode 100644 index 2ef06ec9a..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Input/DataModelStaticViewModel.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System; -using System.Linq; -using System.Windows; -using System.Windows.Input; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.UI.Shared.Services; -using MaterialDesignColors.ColorManipulation; -using Stylet; - -namespace Artemis.UI.Shared.Input -{ - /// - /// Represents a view model that allows inputting a static value used by boolean operations on a certain data model - /// property - /// - public class DataModelStaticViewModel : PropertyChangedBase, IDisposable - { - private readonly IDataModelUIService _dataModelUIService; - private readonly Window? _rootView; - private SolidColorBrush _buttonBrush = new(Color.FromRgb(171, 71, 188)); - private bool _displaySwitchButton; - private DataModelDisplayViewModel? _displayViewModel; - private DataModelInputViewModel? _inputViewModel; - private bool _isEnabled; - private string _placeholder = "Enter a value"; - private DataModelPropertyAttribute _targetDescription; - private Type _targetType; - private object? _value; - - internal DataModelStaticViewModel(Type targetType, DataModelPropertyAttribute targetDescription, IDataModelUIService dataModelUIService) - { - _dataModelUIService = dataModelUIService; - _rootView = Application.Current.Windows.OfType().SingleOrDefault(x => x.IsActive); - - _targetType = targetType; - _targetDescription = targetDescription; - _isEnabled = TargetType != null; - _displayViewModel = _dataModelUIService.GetDataModelDisplayViewModel(TargetType ?? typeof(object), TargetDescription, true); - - if (_rootView != null) - { - _rootView.MouseUp += RootViewOnMouseUp; - _rootView.KeyUp += RootViewOnKeyUp; - } - } - - /// - /// Gets or sets the brush to use for the input button - /// - public SolidColorBrush ButtonBrush - { - get => _buttonBrush; - set - { - if (!SetAndNotify(ref _buttonBrush, value)) return; - NotifyOfPropertyChange(nameof(SwitchButtonBrush)); - } - } - - /// - /// Gets the brush to use for the switch button - /// - public SolidColorBrush SwitchButtonBrush => new(ButtonBrush.Color.Darken()); - - /// - /// Gets the view model used to display the value - /// - public DataModelDisplayViewModel? DisplayViewModel - { - get => _displayViewModel; - private set => SetAndNotify(ref _displayViewModel, value); - } - - /// - /// Gets the view model used to edit the value - /// - public DataModelInputViewModel? InputViewModel - { - get => _inputViewModel; - private set => SetAndNotify(ref _inputViewModel, value); - } - - /// - /// Gets the type of the target property - /// - public Type TargetType - { - get => _targetType; - private set => SetAndNotify(ref _targetType, value); - } - - /// - /// Gets the description of the target property - /// - public DataModelPropertyAttribute TargetDescription - { - get => _targetDescription; - set => SetAndNotify(ref _targetDescription, value); - } - - /// - /// Gets or sets the value of the target - /// - public object? Value - { - get => _value; - set - { - if (!SetAndNotify(ref _value, value)) return; - DisplayViewModel?.UpdateValue(_value); - } - } - - /// - /// Gets or sets the placeholder text when no value is entered - /// - public string Placeholder - { - get => _placeholder; - set => SetAndNotify(ref _placeholder, value); - } - - /// - /// Gets or sets the enabled state of the input - /// - public bool IsEnabled - { - get => _isEnabled; - private set => SetAndNotify(ref _isEnabled, value); - } - - /// - /// Gets or sets whether the switch button should be displayed - /// - public bool DisplaySwitchButton - { - get => _displaySwitchButton; - set => SetAndNotify(ref _displaySwitchButton, value); - } - - /// - /// Activates the input view model - /// - public void ActivateInputViewModel() - { - InputViewModel = _dataModelUIService.GetDataModelInputViewModel( - TargetType, - TargetDescription, - Value, - ApplyFreeInput - ); - } - - /// - /// Updates the target type - /// - /// The new target type - public void UpdateTargetType(Type target) - { - TargetType = target; - DisplayViewModel = _dataModelUIService.GetDataModelDisplayViewModel(TargetType ?? typeof(object), TargetDescription, true); - IsEnabled = TargetType != null; - - // If null, clear the input - if (TargetType == null) - { - ApplyFreeInput(null, true); - return; - } - - // If the type changed, reset to the default type - if (Value == null || !TargetType.IsCastableFrom(Value.GetType())) - // Force the VM to close if it was open and apply the new value - ApplyFreeInput(TargetType.GetDefault(), true); - } - - /// - /// Requests switching the input type to dynamic using a - /// - public void SwitchToDynamic() - { - InputViewModel?.Cancel(); - ApplyFreeInput(TargetType.GetDefault(), true); - - OnSwitchToDynamicRequested(); - } - - private void ApplyFreeInput(object? value, bool submitted) - { - if (submitted) - OnValueUpdated(new DataModelInputStaticEventArgs(value)); - - InputViewModel = null; - Value = value; - } - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_rootView != null) - { - _rootView.MouseUp -= RootViewOnMouseUp; - _rootView.KeyUp -= RootViewOnKeyUp; - } - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - - #region Event handlers - - private void RootViewOnKeyUp(object sender, KeyEventArgs e) - { - if (InputViewModel == null) - return; - - if (e.Key == Key.Escape) - InputViewModel.Cancel(); - else if (e.Key == Key.Enter) - InputViewModel.Submit(); - } - - private void RootViewOnMouseUp(object sender, MouseButtonEventArgs e) - { - if (InputViewModel == null) - return; - - if (sender is FrameworkElement frameworkElement && !frameworkElement.IsDescendantOf(InputViewModel.View)) - InputViewModel.Submit(); - } - - #endregion - - #region Events - - /// - /// Occurs when the value of the property has been updated - /// - public event EventHandler? ValueUpdated; - - /// - /// Occurs when a switch to dynamic input has been requested - /// - public event EventHandler? SwitchToDynamicRequested; - - /// - /// Invokes the event - /// - /// - protected virtual void OnValueUpdated(DataModelInputStaticEventArgs e) - { - ValueUpdated?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnSwitchToDynamicRequested() - { - SwitchToDynamicRequested?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs index bbc85258a..d9060f943 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs @@ -11,15 +11,12 @@ namespace Artemis.UI.Shared /// public class DataModelListPropertiesViewModel : DataModelPropertiesViewModel { - private readonly ListPredicateWrapperDataModel _listPredicateWrapper; private object? _displayValue; private int _index; private Type? _listType; internal DataModelListPropertiesViewModel(Type listType, string? name) : base(null, null, null) { - _listPredicateWrapper = ListPredicateWrapperDataModel.Create(listType, name); - DataModel = _listPredicateWrapper; ListType = listType; } @@ -61,8 +58,6 @@ namespace Artemis.UI.Shared /// public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration) { - _listPredicateWrapper.UntypedValue = DisplayValue; - PopulateProperties(dataModelUIService, configuration); if (DisplayViewModel == null) return; diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs index 2e3c7fe32..29bdeb259 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs @@ -10,22 +10,17 @@ namespace Artemis.UI.Shared /// public class DataModelListPropertyViewModel : DataModelPropertyViewModel { - private readonly ListPredicateWrapperDataModel _listPredicateWrapper; private int _index; private Type? _listType; internal DataModelListPropertyViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null) { - _listPredicateWrapper = ListPredicateWrapperDataModel.Create(listType, name); - DataModel = _listPredicateWrapper; ListType = listType; DisplayViewModel = displayViewModel; } internal DataModelListPropertyViewModel(Type listType, string? name) : base(null, null, null) { - _listPredicateWrapper = ListPredicateWrapperDataModel.Create(listType, name); - DataModel = _listPredicateWrapper; ListType = listType; } @@ -60,8 +55,6 @@ namespace Artemis.UI.Shared if (DisplayValue == null) return; - _listPredicateWrapper.UntypedValue = DisplayValue; - if (DisplayViewModel == null) DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(DisplayValue.GetType(), PropertyDescription, true); diff --git a/src/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs b/src/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs deleted file mode 100644 index 650b558b8..000000000 --- a/src/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Artemis.UI.Shared.Input; - -namespace Artemis.UI.Shared -{ - /// - /// Provides data about submit events raised by - /// - public class DataModelInputStaticEventArgs : EventArgs - { - internal DataModelInputStaticEventArgs(object? value) - { - Value = value; - } - - /// - /// The value that was submitted - /// - public object? Value { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs b/src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs similarity index 50% rename from src/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs rename to src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs index e5a9806de..9306237ed 100644 --- a/src/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs +++ b/src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs @@ -1,22 +1,22 @@ using System; using Artemis.Core; -using Artemis.UI.Shared.Input; +using Artemis.UI.Shared.Controls; namespace Artemis.UI.Shared { /// - /// Provides data about selection events raised by + /// Provides data about selection events raised by /// - public class DataModelInputDynamicEventArgs : EventArgs + public class DataModelSelectedEventArgs : EventArgs { - internal DataModelInputDynamicEventArgs(DataModelPath? dataModelPath) - { - DataModelPath = dataModelPath; - } - /// /// Gets the data model path that was selected /// - public DataModelPath? DataModelPath { get; } + public DataModelPath? Path { get; } + + internal DataModelSelectedEventArgs(DataModelPath? path) + { + Path = path; + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Extensions/DataModelWrapperExtensions.cs b/src/Artemis.UI.Shared/Extensions/DataModelWrapperExtensions.cs deleted file mode 100644 index d21024543..000000000 --- a/src/Artemis.UI.Shared/Extensions/DataModelWrapperExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Linq; -using Artemis.Core; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Shared -{ - /// - /// Provides extensions for special data model wrappers used by events and list conditions - /// - public static class DataModelWrapperExtensions - { - /// - /// Creates a view model for a - /// - /// The wrapper to create the view model for - /// The data model UI service to be used by the view model - /// The update configuration to be used by the view model - /// The created view model - public static DataModelPropertiesViewModel CreateViewModel(this EventPredicateWrapperDataModel wrapper, IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration) - { - DataModelPropertiesViewModel viewModel = new(wrapper, null, new DataModelPath(wrapper)); - viewModel.Update(dataModelUIService, configuration); - viewModel.UpdateRequested += (sender, args) => viewModel.Update(dataModelUIService, configuration); - viewModel.Children.First().IsVisualizationExpanded = true; - - return viewModel; - } - - /// - /// Creates a view model for a - /// - /// The wrapper to create the view model for - /// The data model UI service to be used by the view model - /// The update configuration to be used by the view model - /// The created view model - public static DataModelPropertiesViewModel CreateViewModel(this ListPredicateWrapperDataModel wrapper, IDataModelUIService dataModelUIService, DataModelUpdateConfiguration configuration) - { - DataModelPropertiesViewModel viewModel = new(wrapper, null, new DataModelPath(wrapper)); - viewModel.Update(dataModelUIService, configuration); - viewModel.UpdateRequested += (sender, args) => viewModel.Update(dataModelUIService, configuration); - viewModel.Children.First().IsVisualizationExpanded = true; - - return viewModel; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs b/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs index 21cce1e60..228b34c6e 100644 --- a/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs +++ b/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using Artemis.Core.Modules; -using Artemis.UI.Shared.Input; - -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared { /// /// Represents a factory for view models provided by the Artemis Shared UI library @@ -11,25 +6,4 @@ namespace Artemis.UI.Shared public interface ISharedVmFactory { } - - /// - /// A factory that allows the creation of data model view models - /// - public interface IDataModelVmFactory : ISharedVmFactory - { - /// - /// Creates a new instance of the class - /// - /// The modules to associate the dynamic view model with - /// A new instance of the class - DataModelDynamicViewModel DataModelDynamicViewModel(List modules); - - /// - /// Creates a new instance of the class - /// - /// The type of property that is expected in this input - /// The description of the property that this input is for - /// A new instance of the class - DataModelStaticViewModel DataModelStaticViewModel(Type targetType, DataModelPropertyAttribute targetDescription); - } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/DataModelUIService.cs b/src/Artemis.UI.Shared/Services/DataModelUIService.cs index dc8e5ca7e..db4c0d8bd 100644 --- a/src/Artemis.UI.Shared/Services/DataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/DataModelUIService.cs @@ -5,7 +5,6 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Shared.DefaultTypes.DataModel.Display; -using Artemis.UI.Shared.Input; using Ninject; using Ninject.Parameters; @@ -14,15 +13,13 @@ namespace Artemis.UI.Shared.Services internal class DataModelUIService : IDataModelUIService { private readonly IDataModelService _dataModelService; - private readonly IDataModelVmFactory _dataModelVmFactory; private readonly IKernel _kernel; private readonly List _registeredDataModelDisplays; private readonly List _registeredDataModelEditors; - public DataModelUIService(IDataModelService dataModelService, IDataModelVmFactory dataModelVmFactory, IKernel kernel) + public DataModelUIService(IDataModelService dataModelService, IKernel kernel) { _dataModelService = dataModelService; - _dataModelVmFactory = dataModelVmFactory; _kernel = kernel; _registeredDataModelEditors = new List(); _registeredDataModelDisplays = new List(); @@ -223,22 +220,7 @@ namespace Artemis.UI.Shared.Services return null; } } - - public DataModelDynamicViewModel GetDynamicSelectionViewModel(Module? module) - { - return _dataModelVmFactory.DataModelDynamicViewModel(module == null ? new List() : new List {module}); - } - - public DataModelDynamicViewModel GetDynamicSelectionViewModel(List modules) - { - return _dataModelVmFactory.DataModelDynamicViewModel(modules); - } - - public DataModelStaticViewModel GetStaticInputViewModel(Type targetType, DataModelPropertyAttribute targetDescription) - { - return _dataModelVmFactory.DataModelStaticViewModel(targetType, targetDescription); - } - + private DataModelInputViewModel InstantiateDataModelInputViewModel(DataModelVisualizationRegistration registration, DataModelPropertyAttribute? description, object? initialValue) { // This assumes the type can be converted, that has been checked when the VM was created diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs index 2c3f6efdb..962da118d 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IDataModelUIService.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Artemis.Core; using Artemis.Core.Modules; -using Artemis.UI.Shared.Input; namespace Artemis.UI.Shared.Services { @@ -103,27 +102,5 @@ namespace Artemis.UI.Shared.Services /// A function to call whenever the input was updated (submitted or not) /// The most appropriate input view model for the provided DataModelInputViewModel? GetDataModelInputViewModel(Type propertyType, DataModelPropertyAttribute? description, object? initialValue, Action updateCallback); - - /// - /// Creates a view model that allows selecting a value from the data model - /// - /// An extra non-always active module to include - /// - DataModelDynamicViewModel GetDynamicSelectionViewModel(Module? module); - - /// - /// Creates a view model that allows selecting a value from the data model - /// - /// A list of extra extra non-always active modules to include - /// A view model that allows selecting a value from the data model - DataModelDynamicViewModel GetDynamicSelectionViewModel(List modules); - - /// - /// Creates a view model that allows entering a value matching the target data model type - /// - /// The type of data model property to allow input for - /// The description of the target data model property - /// A view model that allows entering a value matching the target data model type - DataModelStaticViewModel GetStaticInputViewModel(Type targetType, DataModelPropertyAttribute targetDescription); } } \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 375efa7f4..7d1c85aa3 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,15 +1,10 @@ -using System.Collections.Generic; -using Artemis.Core; +using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.ScriptingProviders; using Artemis.UI.Screens.Header; using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.ProfileEditor.Conditions; -using Artemis.UI.Screens.ProfileEditor.DisplayConditions; using Artemis.UI.Screens.ProfileEditor.LayerProperties; using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.ConditionalDataBinding; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding; using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree; @@ -83,16 +78,6 @@ namespace Artemis.UI.Ninject.Factories SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel); } - public interface IDataModelConditionsVmFactory : IVmFactory - { - DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType, List modules); - DataModelConditionListViewModel DataModelConditionListViewModel(DataModelConditionList dataModelConditionList, List modules); - DataModelConditionEventViewModel DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, List modules); - DataModelConditionGeneralPredicateViewModel DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate, List modules); - DataModelConditionListPredicateViewModel DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate, List modules); - DataModelConditionEventPredicateViewModel DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate, List modules); - } - public interface ILayerPropertyVmFactory : IVmFactory { LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty); @@ -131,17 +116,9 @@ namespace Artemis.UI.Ninject.Factories NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript); } - // TODO: Move these two public interface IDataBindingsVmFactory { IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration); - DirectDataBindingModeViewModel DirectDataBindingModeViewModel(DirectDataBinding directDataBinding); - DataBindingModifierViewModel DataBindingModifierViewModel(DataBindingModifier modifier); - - ConditionalDataBindingModeViewModel ConditionalDataBindingModeViewModel( - ConditionalDataBinding conditionalDataBinding); - - DataBindingConditionViewModel DataBindingConditionViewModel(DataBindingCondition dataBindingCondition); } public interface IPropertyVmFactory diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs deleted file mode 100644 index 9995cd02f..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Input; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract -{ - public abstract class DataModelConditionPredicateViewModel : DataModelConditionViewModel, IDisposable - { - private readonly IConditionOperatorService _conditionOperatorService; - private readonly IDataModelUIService _dataModelUIService; - private readonly List _modules; - private readonly IProfileEditorService _profileEditorService; - private DataModelStaticViewModel _rightSideInputViewModel; - private DataModelDynamicViewModel _rightSideSelectionViewModel; - private BaseConditionOperator _selectedOperator; - - private List _supportedInputTypes; - - protected DataModelConditionPredicateViewModel( - DataModelConditionPredicate dataModelConditionPredicate, - List modules, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IConditionOperatorService conditionOperatorService, - ISettingsService settingsService) : base(dataModelConditionPredicate) - { - _modules = modules; - _profileEditorService = profileEditorService; - _dataModelUIService = dataModelUIService; - _conditionOperatorService = conditionOperatorService; - _supportedInputTypes = new List(); - - SelectOperatorCommand = new DelegateCommand(ExecuteSelectOperatorCommand); - Operators = new BindableCollection(); - - ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues"); - } - - public DataModelConditionPredicate DataModelConditionPredicate => (DataModelConditionPredicate) Model; - public PluginSetting ShowDataModelValues { get; } - - public bool CanSelectOperator => DataModelConditionPredicate.LeftPath is {IsValid: true}; - - public BaseConditionOperator SelectedOperator - { - get => _selectedOperator; - set => SetAndNotify(ref _selectedOperator, value); - } - - public DataModelDynamicViewModel RightSideSelectionViewModel - { - get => _rightSideSelectionViewModel; - set => SetAndNotify(ref _rightSideSelectionViewModel, value); - } - - public DataModelStaticViewModel RightSideInputViewModel - { - get => _rightSideInputViewModel; - set => SetAndNotify(ref _rightSideInputViewModel, value); - } - - public DelegateCommand SelectOperatorCommand { get; } - public BindableCollection Operators { get; } - - protected SolidColorBrush LeftSideColor { get; set; } - - public override void Evaluate() - { - IsConditionMet = DataModelConditionPredicate.Evaluate(); - } - - public override void Delete() - { - base.Delete(); - _profileEditorService.SaveSelectedProfileElement(); - } - - public virtual void Initialize() - { - LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); - LeftSideSelectionViewModel.PropertySelected += LeftSideOnPropertySelected; - if (LeftSideColor != null) - LeftSideSelectionViewModel.ButtonBrush = LeftSideColor; - - // Determine which types are currently supported - _supportedInputTypes = GetSupportedInputTypes(); - - Update(); - } - - public override void Update() - { - LeftSideSelectionViewModel.FilterTypes = _supportedInputTypes.ToArray(); - LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionPredicate.LeftPath); - - Type leftSideType = GetLeftSideType(); - - // Get the supported operators - Operators.Clear(); - Operators.AddRange(_conditionOperatorService.GetConditionOperatorsForType(leftSideType ?? typeof(object), ConditionParameterSide.Left)); - if (DataModelConditionPredicate.Operator == null) - DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault()); - // The core doesn't care about best matches so if there is a new preferred operator, use that instead - else if (!Operators.Contains(DataModelConditionPredicate.Operator)) - DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.Description == DataModelConditionPredicate.Operator.Description) ?? Operators.FirstOrDefault()); - - NotifyOfPropertyChange(nameof(CanSelectOperator)); - SelectedOperator = DataModelConditionPredicate.Operator; - - // Without a selected operator or one that supports a right side, leave the right side input empty - if (SelectedOperator?.RightSideType == null) - { - DisposeRightSideStaticViewModel(); - DisposeRightSideDynamicViewModel(); - return; - } - - // Ensure the right side has the proper VM - if (DataModelConditionPredicate.PredicateType == ProfileRightSideType.Dynamic) - { - DisposeRightSideStaticViewModel(); - if (RightSideSelectionViewModel == null) - CreateRightSideSelectionViewModel(); - - RightSideSelectionViewModel.ChangeDataModelPath(DataModelConditionPredicate.RightPath); - RightSideSelectionViewModel.FilterTypes = new[] {SelectedOperator.RightSideType}; - } - else - { - DisposeRightSideDynamicViewModel(); - if (RightSideInputViewModel == null) - CreateRightSideInputViewModel(); - - Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType(); - if (preferredType != null && RightSideInputViewModel.TargetType != preferredType) - RightSideInputViewModel.UpdateTargetType(preferredType); - - RightSideInputViewModel.Value = DataModelConditionPredicate.RightStaticValue; - } - } - - public void ApplyLeftSide() - { - Type newType = LeftSideSelectionViewModel.DataModelPath.GetPropertyType(); - bool converted = ConvertIfRequired(newType); - if (converted) - return; - - DataModelConditionPredicate.UpdateLeftSide(LeftSideSelectionViewModel.DataModelPath); - _profileEditorService.SaveSelectedProfileElement(); - - SelectedOperator = DataModelConditionPredicate.Operator; - Update(); - } - - public void ApplyRightSideDynamic() - { - DataModelConditionPredicate.UpdateRightSideDynamic(RightSideSelectionViewModel.DataModelPath); - _profileEditorService.SaveSelectedProfileElement(); - - Update(); - } - - public void ApplyRightSideStatic(object value) - { - DataModelConditionPredicate.UpdateRightSideStatic(value); - _profileEditorService.SaveSelectedProfileElement(); - - Update(); - } - - public void ApplyOperator() - { - DataModelConditionPredicate.UpdateOperator(SelectedOperator); - _profileEditorService.SaveSelectedProfileElement(); - - Update(); - } - - protected abstract List GetSupportedInputTypes(); - protected abstract Type GetLeftSideType(); - - protected virtual List GetExtraRightSideDataModelViewModels() - { - return null; - } - - private void ExecuteSelectOperatorCommand(object context) - { - if (!(context is BaseConditionOperator dataModelConditionOperator)) - return; - - SelectedOperator = dataModelConditionOperator; - ApplyOperator(); - } - - public override void UpdateModules() - { - if (LeftSideSelectionViewModel != null) - { - LeftSideSelectionViewModel.PropertySelected -= LeftSideOnPropertySelected; - LeftSideSelectionViewModel.Dispose(); - LeftSideSelectionViewModel = null; - } - DisposeRightSideStaticViewModel(); - DisposeRightSideDynamicViewModel(); - - // If the modules changed the paths may no longer be valid if they targeted a module no longer available, in that case clear the path - if (DataModelConditionPredicate.LeftPath?.Target != null && !DataModelConditionPredicate.LeftPath.Target.IsExpansion && !_modules.Contains(DataModelConditionPredicate.LeftPath.Target.Module)) - DataModelConditionPredicate.UpdateLeftSide(null); - if (DataModelConditionPredicate.RightPath?.Target != null && !DataModelConditionPredicate.RightPath.Target.IsExpansion && !_modules.Contains(DataModelConditionPredicate.RightPath.Target.Module)) - DataModelConditionPredicate.UpdateRightSideDynamic(null); - - Initialize(); - } - - #region IDisposable - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (LeftSideSelectionViewModel != null) - { - LeftSideSelectionViewModel.PropertySelected -= LeftSideOnPropertySelected; - LeftSideSelectionViewModel.Dispose(); - LeftSideSelectionViewModel = null; - } - - DisposeRightSideStaticViewModel(); - DisposeRightSideDynamicViewModel(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - #region View model creation - - private void CreateRightSideSelectionViewModel() - { - RightSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); - RightSideSelectionViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush"); - RightSideSelectionViewModel.DisplaySwitchButton = true; - RightSideSelectionViewModel.PropertySelected += RightSideOnPropertySelected; - RightSideSelectionViewModel.SwitchToStaticRequested += RightSideSelectionViewModelOnSwitchToStaticRequested; - - List extra = GetExtraRightSideDataModelViewModels(); - if (extra != null) - RightSideSelectionViewModel.ExtraDataModelViewModels.AddRange(extra); - } - - private void CreateRightSideInputViewModel() - { - Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType(); - RightSideInputViewModel = _dataModelUIService.GetStaticInputViewModel(preferredType, LeftSideSelectionViewModel.DataModelPath?.GetPropertyDescription()); - RightSideInputViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush"); - RightSideInputViewModel.DisplaySwitchButton = true; - RightSideInputViewModel.ValueUpdated += RightSideOnValueEntered; - RightSideInputViewModel.SwitchToDynamicRequested += RightSideInputViewModelOnSwitchToDynamicRequested; - } - - private void DisposeRightSideStaticViewModel() - { - if (RightSideInputViewModel == null) - return; - RightSideInputViewModel.ValueUpdated -= RightSideOnValueEntered; - RightSideInputViewModel.SwitchToDynamicRequested -= RightSideInputViewModelOnSwitchToDynamicRequested; - RightSideInputViewModel.Dispose(); - RightSideInputViewModel = null; - } - - private void DisposeRightSideDynamicViewModel() - { - if (RightSideSelectionViewModel == null) - return; - RightSideSelectionViewModel.PropertySelected -= RightSideOnPropertySelected; - RightSideSelectionViewModel.SwitchToStaticRequested -= RightSideSelectionViewModelOnSwitchToStaticRequested; - RightSideSelectionViewModel.Dispose(); - RightSideSelectionViewModel = null; - } - - #endregion - - #region Event handlers - - private void LeftSideOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) - { - ApplyLeftSide(); - } - - private void RightSideOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) - { - ApplyRightSideDynamic(); - } - - private void RightSideOnValueEntered(object sender, DataModelInputStaticEventArgs e) - { - ApplyRightSideStatic(e.Value); - } - - private void RightSideSelectionViewModelOnSwitchToStaticRequested(object sender, EventArgs e) - { - DataModelConditionPredicate.PredicateType = ProfileRightSideType.Static; - - // Ensure the right static value is never null when the preferred type is a value type - Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType(); - if (DataModelConditionPredicate.RightStaticValue == null && preferredType != null && preferredType.IsValueType) - DataModelConditionPredicate.UpdateRightSideStatic(preferredType.GetDefault()); - - Update(); - } - - private void RightSideInputViewModelOnSwitchToDynamicRequested(object sender, EventArgs e) - { - DataModelConditionPredicate.PredicateType = ProfileRightSideType.Dynamic; - Update(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs deleted file mode 100644 index 9f7db7a2d..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionViewModel.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Artemis.Core; -using Artemis.UI.Shared.Input; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract -{ - public abstract class DataModelConditionViewModel : Conductor.Collection.AllActive - { - private DataModelDynamicViewModel _leftSideSelectionViewModel; - private bool _isConditionMet; - - protected DataModelConditionViewModel(DataModelConditionPart model) - { - Model = model; - } - - public DataModelConditionPart Model { get; } - - public DataModelDynamicViewModel LeftSideSelectionViewModel - { - get => _leftSideSelectionViewModel; - set => SetAndNotify(ref _leftSideSelectionViewModel, value); - } - - public bool IsConditionMet - { - get => _isConditionMet; - set => SetAndNotify(ref _isConditionMet, value); - } - - - public abstract void Update(); - - public abstract void Evaluate(); - - public virtual void Delete() - { - Model.Parent.RemoveChild(Model); - ((DataModelConditionViewModel) Parent).Update(); - } - - protected bool ConvertIfRequired(Type newType) - { - if (newType == null) - return false; - - if (!(Parent is DataModelConditionGroupViewModel groupViewModel)) - return false; - - // List - if (newType.IsGenericEnumerable()) - { - if (this is DataModelConditionListViewModel) - return false; - groupViewModel.ConvertToConditionList(this); - return true; - } - - // Predicate - if (this is DataModelConditionPredicateViewModel) - return false; - groupViewModel.ConvertToPredicate(this); - return true; - } - - public abstract void UpdateModules(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventView.xaml deleted file mode 100644 index 0c93f64b5..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventView.xaml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs deleted file mode 100644 index d50edbbc9..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionEventViewModel.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - public sealed class DataModelConditionEventViewModel : DataModelConditionViewModel, IDisposable - { - private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; - private readonly IDataModelUIService _dataModelUIService; - private readonly List _modules; - private readonly IProfileEditorService _profileEditorService; - private DateTime _lastTrigger; - private string _triggerPastParticiple; - - public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, - List modules, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionEvent) - { - _modules = modules; - _profileEditorService = profileEditorService; - _dataModelUIService = dataModelUIService; - _dataModelConditionsVmFactory = dataModelConditionsVmFactory; - - _lastTrigger = DataModelConditionEvent.LastTrigger; - } - - public DataModelConditionEvent DataModelConditionEvent => (DataModelConditionEvent) Model; - - public DateTime LastTrigger - { - get => _lastTrigger; - set => SetAndNotify(ref _lastTrigger, value); - } - - public string TriggerPastParticiple - { - get => _triggerPastParticiple; - set => SetAndNotify(ref _triggerPastParticiple, value); - } - - public void Initialize() - { - LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); - LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected; - LeftSideSelectionViewModel.LoadEventChildren = false; - - IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; - List supportedInputTypes = editors.Select(e => e.SupportedType).ToList(); - supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes)); - supportedInputTypes.Add(typeof(DataModelEvent)); - supportedInputTypes.Add(typeof(DataModelEvent<>)); - - LeftSideSelectionViewModel.FilterTypes = supportedInputTypes.ToArray(); - LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(185, 164, 10)); - LeftSideSelectionViewModel.Placeholder = "Select an event"; - - Update(); - } - - public override void Update() - { - LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionEvent.EventPath); - - // Remove VMs of effects no longer applied on the layer - Items.RemoveRange(Items.Where(c => !DataModelConditionEvent.Children.Contains(c.Model)).ToList()); - - if (DataModelConditionEvent.EventPath == null || !DataModelConditionEvent.EventPath.IsValid) - return; - - TriggerPastParticiple = DataModelConditionEvent.GetDataModelEvent()?.TriggerPastParticiple; - List viewModels = new(); - foreach (DataModelConditionPart childModel in Model.Children) - { - if (Items.Any(c => c.Model == childModel)) - continue; - if (!(childModel is DataModelConditionGroup dataModelConditionGroup)) - continue; - - DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.Event, _modules); - viewModel.IsRootGroup = true; - viewModels.Add(viewModel); - } - - if (viewModels.Any()) - Items.AddRange(viewModels); - - foreach (DataModelConditionViewModel childViewModel in Items) - childViewModel.Update(); - } - - public override void Evaluate() - { - LastTrigger = DataModelConditionEvent.LastTrigger; - IsConditionMet = DataModelConditionEvent.Evaluate(); - } - - public void ApplyEvent() - { - DataModelConditionEvent.UpdateEvent(LeftSideSelectionViewModel.DataModelPath); - _profileEditorService.SaveSelectedProfileElement(); - - Update(); - } - - public override void UpdateModules() - { - LeftSideSelectionViewModel.Dispose(); - LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected; - Initialize(); - } - - protected override void OnInitialActivate() - { - Initialize(); - base.OnInitialActivate(); - } - - #region Event handlers - - private void LeftSideSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) - { - ApplyEvent(); - } - - #endregion - - #region IDisposable - - public void Dispose() - { - LeftSideSelectionViewModel.Dispose(); - LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml deleted file mode 100644 index 7afa0ef9a..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupView.xaml +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs deleted file mode 100644 index 6b0e8c1ab..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionGroupViewModel.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; -using Artemis.UI.Shared.Services; -using Humanizer; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - public class DataModelConditionGroupViewModel : DataModelConditionViewModel - { - private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; - private readonly List _modules; - private readonly ICoreService _coreService; - private readonly IProfileEditorService _profileEditorService; - private bool _isEventGroup; - private bool _isRootGroup; - - public DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, - ConditionGroupType groupType, - List modules, - ICoreService coreService, - IProfileEditorService profileEditorService, - IDataModelConditionsVmFactory dataModelConditionsVmFactory) - : base(dataModelConditionGroup) - { - GroupType = groupType; - _modules = modules; - _coreService = coreService; - _profileEditorService = profileEditorService; - _dataModelConditionsVmFactory = dataModelConditionsVmFactory; - - Items.CollectionChanged += (_, _) => NotifyOfPropertyChange(nameof(DisplayBooleanOperator)); - } - - public ConditionGroupType GroupType { get; } - public DataModelConditionGroup DataModelConditionGroup => (DataModelConditionGroup) Model; - - public bool IsRootGroup - { - get => _isRootGroup; - set - { - if (!SetAndNotify(ref _isRootGroup, value)) return; - NotifyOfPropertyChange(nameof(CanAddEventCondition)); - } - } - - public bool CanAddEventCondition => IsRootGroup && GroupType == ConditionGroupType.General; - - public bool IsEventGroup - { - get => _isEventGroup; - set - { - SetAndNotify(ref _isEventGroup, value); - NotifyOfPropertyChange(nameof(DisplayEvaluationResult)); - } - } - - public bool DisplayBooleanOperator => Items.Count > 1; - public bool DisplayEvaluationResult => GroupType == ConditionGroupType.General && !IsEventGroup; - public string SelectedBooleanOperator => DataModelConditionGroup.BooleanOperator.Humanize(); - - public void SelectBooleanOperator(string type) - { - BooleanOperator enumValue = Enum.Parse(type); - DataModelConditionGroup.BooleanOperator = enumValue; - NotifyOfPropertyChange(nameof(SelectedBooleanOperator)); - - _profileEditorService.SaveSelectedProfileElement(); - } - - public void AddCondition() - { - switch (GroupType) - { - case ConditionGroupType.General: - DataModelConditionGroup.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic)); - break; - case ConditionGroupType.List: - DataModelConditionGroup.AddChild(new DataModelConditionListPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic)); - break; - case ConditionGroupType.Event: - DataModelConditionGroup.AddChild(new DataModelConditionEventPredicate(DataModelConditionGroup, ProfileRightSideType.Dynamic)); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - Update(); - _profileEditorService.SaveSelectedProfileElement(); - } - - public void AddEventCondition() - { - if (!CanAddEventCondition) - return; - - // Find a good spot for the event, behind the last existing event - int index = 0; - DataModelConditionPart existing = DataModelConditionGroup.Children.LastOrDefault(c => c is DataModelConditionEvent); - if (existing != null) - index = DataModelConditionGroup.Children.IndexOf(existing) + 1; - - DataModelConditionGroup.AddChild(new DataModelConditionEvent(DataModelConditionGroup), index); - - Update(); - _profileEditorService.SaveSelectedProfileElement(); - } - - public void AddGroup() - { - DataModelConditionGroup.AddChild(new DataModelConditionGroup(DataModelConditionGroup)); - - Update(); - _profileEditorService.SaveSelectedProfileElement(); - } - - public override void Update() - { - NotifyOfPropertyChange(nameof(SelectedBooleanOperator)); - // Remove VMs of effects no longer applied on the layer - List toRemove = Items.Where(c => !DataModelConditionGroup.Children.Contains(c.Model)).ToList(); - if (toRemove.Any()) - Items.RemoveRange(toRemove); - - foreach (DataModelConditionPart childModel in Model.Children) - { - if (Items.Any(c => c.Model == childModel)) - continue; - - switch (childModel) - { - case DataModelConditionGroup dataModelConditionGroup: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, GroupType, _modules)); - break; - case DataModelConditionList dataModelConditionList: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(dataModelConditionList, _modules)); - break; - case DataModelConditionEvent dataModelConditionEvent: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventViewModel(dataModelConditionEvent, _modules)); - break; - case DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionGeneralPredicateViewModel(dataModelConditionGeneralPredicate, _modules)); - break; - case DataModelConditionListPredicate dataModelConditionListPredicate: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(dataModelConditionListPredicate, _modules)); - break; - case DataModelConditionEventPredicate dataModelConditionEventPredicate: - Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventPredicateViewModel(dataModelConditionEventPredicate, _modules)); - break; - } - } - - // Ensure the items are in the same order as on the model - ((BindableCollection) Items).Sort(i => Model.Children.IndexOf(i.Model)); - foreach (DataModelConditionViewModel childViewModel in Items) - childViewModel.Update(); - - IsEventGroup = Items.Any(i => i is DataModelConditionEventViewModel); - if (IsEventGroup) - { - if (DataModelConditionGroup.BooleanOperator != BooleanOperator.And) - SelectBooleanOperator("And"); - } - - OnUpdated(); - } - - public override void Evaluate() - { - IsConditionMet = DataModelConditionGroup.Evaluate(); - foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) - dataModelConditionViewModel.Evaluate(); - } - - public override void UpdateModules() - { - foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) - dataModelConditionViewModel.UpdateModules(); - } - - public void ConvertToConditionList(DataModelConditionViewModel predicateViewModel) - { - // Store the old index and remove the old predicate - int index = DataModelConditionGroup.Children.IndexOf(predicateViewModel.Model); - DataModelConditionGroup.RemoveChild(predicateViewModel.Model); - - // Insert a list in the same position - DataModelConditionList list = new(DataModelConditionGroup); - list.UpdateList(predicateViewModel.LeftSideSelectionViewModel.DataModelPath); - DataModelConditionGroup.AddChild(list, index); - - // Update to switch the VMs - Update(); - } - - public void ConvertToPredicate(DataModelConditionViewModel listViewModel) - { - // Store the old index and remove the old predicate - int index = DataModelConditionGroup.Children.IndexOf(listViewModel.Model); - DataModelConditionGroup.RemoveChild(listViewModel.Model); - - // Insert a list in the same position - DataModelConditionGeneralPredicate predicate = new(DataModelConditionGroup, ProfileRightSideType.Dynamic); - predicate.UpdateLeftSide(listViewModel.LeftSideSelectionViewModel.DataModelPath); - DataModelConditionGroup.AddChild(predicate, index); - - // Update to switch the VMs - Update(); - } - - private void CoreServiceOnFrameRendered(object sender, FrameRenderedEventArgs e) - { - if (IsRootGroup) - Evaluate(); - } - - public event EventHandler Updated; - - #region Overrides of Screen - - /// - protected override void OnInitialActivate() - { - base.OnInitialActivate(); - Update(); - _coreService.FrameRendered += CoreServiceOnFrameRendered; - } - - /// - protected override void OnClose() - { - _coreService.FrameRendered -= CoreServiceOnFrameRendered; - base.OnClose(); - } - - #endregion - - protected virtual void OnUpdated() - { - Updated?.Invoke(this, EventArgs.Empty); - } - } - - public enum ConditionGroupType - { - General, - List, - Event - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml deleted file mode 100644 index ac405354f..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml.cs deleted file mode 100644 index 23a1fe9e6..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListView.xaml.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - /// - /// Interaction logic for DataModelConditionListView.xaml - /// - public partial class DataModelConditionListView : UserControl - { - public DataModelConditionListView() - { - InitializeComponent(); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs deleted file mode 100644 index c4e244b66..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/DataModelConditionListViewModel.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; -using Humanizer; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - public sealed class DataModelConditionListViewModel : DataModelConditionViewModel, IDisposable - { - private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; - private readonly IDataModelUIService _dataModelUIService; - private readonly List _modules; - private readonly IProfileEditorService _profileEditorService; - - public DataModelConditionListViewModel(DataModelConditionList dataModelConditionList, - List modules, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionList) - { - _modules = modules; - _profileEditorService = profileEditorService; - _dataModelUIService = dataModelUIService; - _dataModelConditionsVmFactory = dataModelConditionsVmFactory; - } - - public DataModelConditionList DataModelConditionList => (DataModelConditionList) Model; - - public string SelectedListOperator => DataModelConditionList.ListOperator.Humanize(); - - public void SelectListOperator(string type) - { - ListOperator enumValue = Enum.Parse(type); - DataModelConditionList.ListOperator = enumValue; - NotifyOfPropertyChange(nameof(SelectedListOperator)); - - _profileEditorService.SaveSelectedProfileElement(); - } - - public void AddCondition() - { - DataModelConditionList.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionList, ProfileRightSideType.Dynamic)); - - Update(); - _profileEditorService.SaveSelectedProfileElement(); - } - - public void AddGroup() - { - DataModelConditionList.AddChild(new DataModelConditionGroup(DataModelConditionList)); - - Update(); - _profileEditorService.SaveSelectedProfileElement(); - } - - public override void Evaluate() - { - IsConditionMet = DataModelConditionList.Evaluate(); - foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) - dataModelConditionViewModel.Evaluate(); - } - - public override void Delete() - { - base.Delete(); - _profileEditorService.SaveSelectedProfileElement(); - } - - /// - public override void UpdateModules() - { - foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) - dataModelConditionViewModel.UpdateModules(); - } - - public void Initialize() - { - LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules); - LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected; - - IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; - List supportedInputTypes = editors.Select(e => e.SupportedType).ToList(); - supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes)); - supportedInputTypes.Add(typeof(IEnumerable<>)); - - LeftSideSelectionViewModel.FilterTypes = supportedInputTypes.ToArray(); - LeftSideSelectionViewModel.ButtonBrush = new SolidColorBrush(Color.FromRgb(71, 108, 188)); - LeftSideSelectionViewModel.Placeholder = "Select a list"; - - Update(); - } - - public void ApplyList() - { - Type newType = LeftSideSelectionViewModel.DataModelPath.GetPropertyType(); - bool converted = ConvertIfRequired(newType); - if (converted) - return; - - DataModelConditionList.UpdateList(LeftSideSelectionViewModel.DataModelPath); - _profileEditorService.SaveSelectedProfileElement(); - - Update(); - } - - public override void Update() - { - LeftSideSelectionViewModel.ChangeDataModelPath(DataModelConditionList.ListPath); - NotifyOfPropertyChange(nameof(SelectedListOperator)); - - // Remove VMs of effects no longer applied on the layer - Items.RemoveRange(Items.Where(c => !DataModelConditionList.Children.Contains(c.Model)).ToList()); - - if (DataModelConditionList.ListPath == null || !DataModelConditionList.ListPath.IsValid) - return; - - List viewModels = new(); - foreach (DataModelConditionPart childModel in Model.Children) - { - if (Items.Any(c => c.Model == childModel)) - continue; - if (!(childModel is DataModelConditionGroup dataModelConditionGroup)) - continue; - - DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.List, _modules); - viewModel.IsRootGroup = true; - viewModels.Add(viewModel); - } - - if (viewModels.Any()) - Items.AddRange(viewModels); - - foreach (DataModelConditionViewModel childViewModel in Items) - childViewModel.Update(); - } - - protected override void OnInitialActivate() - { - Initialize(); - base.OnInitialActivate(); - } - - private void LeftSideSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) - { - ApplyList(); - } - - #region IDisposable - - public void Dispose() - { - LeftSideSelectionViewModel.Dispose(); - LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml deleted file mode 100644 index a90fc0b13..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml.cs deleted file mode 100644 index 5ce43cf28..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateView.xaml.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - /// - /// Interaction logic for DataModelConditionEventPredicateView.xaml - /// - public partial class DataModelConditionEventPredicateView : UserControl - { - public DataModelConditionEventPredicateView() - { - InitializeComponent(); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs deleted file mode 100644 index 78bd7b625..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionEventPredicateViewModel.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Extensions; -using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - public class DataModelConditionEventPredicateViewModel : DataModelConditionPredicateViewModel - { - private readonly IDataModelUIService _dataModelUIService; - - public DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate, - List modules, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IConditionOperatorService conditionOperatorService, - ISettingsService settingsService) - : base(dataModelConditionEventPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) - { - _dataModelUIService = dataModelUIService; - - LeftSideColor = new SolidColorBrush(Color.FromRgb(185, 164, 10)); - } - - public DataModelConditionEventPredicate DataModelConditionEventPredicate => (DataModelConditionEventPredicate) Model; - - public override void Initialize() - { - base.Initialize(); - - DataModelPropertiesViewModel eventDataModel = GetEventDataModel(); - LeftSideSelectionViewModel.ChangeDataModel(eventDataModel); - } - - protected override void OnInitialActivate() - { - base.OnInitialActivate(); - Initialize(); - } - - protected override List GetSupportedInputTypes() - { - IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; - List supportedInputTypes = editors.Select(e => e.SupportedType).ToList(); - supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes)); - - return supportedInputTypes; - } - - protected override Type GetLeftSideType() - { - return LeftSideSelectionViewModel.DataModelPath?.GetPropertyType(); - } - - protected override List GetExtraRightSideDataModelViewModels() - { - // Only the VM housing the event arguments is expected here which is a DataModelPropertiesViewModel - return GetEventDataModel().Children.Cast().ToList(); - } - - private DataModelPropertiesViewModel GetEventDataModel() - { - EventPredicateWrapperDataModel wrapper = EventPredicateWrapperDataModel.Create( - DataModelConditionEventPredicate.DataModelConditionEvent.EventArgumentType - ); - - return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(false)); - } - - public override void Evaluate() - { - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml deleted file mode 100644 index eab1e69d3..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml.cs deleted file mode 100644 index 22411462b..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateView.xaml.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - /// - /// Interaction logic for DataModelConditionGeneralPredicateView.xaml - /// - public partial class DataModelConditionGeneralPredicateView : UserControl - { - public DataModelConditionGeneralPredicateView() - { - InitializeComponent(); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs deleted file mode 100644 index f02494b7f..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionGeneralPredicateViewModel.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - public class DataModelConditionGeneralPredicateViewModel : DataModelConditionPredicateViewModel - { - private readonly IDataModelUIService _dataModelUIService; - - public DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate, - List modules, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IConditionOperatorService conditionOperatorService, - ISettingsService settingsService) - : base(dataModelConditionGeneralPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) - { - _dataModelUIService = dataModelUIService; - } - - protected override void OnInitialActivate() - { - Initialize(); - } - - protected override List GetSupportedInputTypes() - { - IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; - List supportedInputTypes = editors.Select(e => e.SupportedType).ToList(); - supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes)); - supportedInputTypes.Add(typeof(IEnumerable<>)); - - return supportedInputTypes; - } - - protected override Type GetLeftSideType() - { - return LeftSideSelectionViewModel.DataModelPath?.GetPropertyType(); - } - - public override void Evaluate() - { - IsConditionMet = DataModelConditionPredicate.Evaluate(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml deleted file mode 100644 index 5a273a099..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml.cs deleted file mode 100644 index 44c02f06b..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateView.xaml.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - /// - /// Interaction logic for DataModelConditionListPredicateView.xaml - /// - public partial class DataModelConditionListPredicateView : UserControl - { - public DataModelConditionListPredicateView() - { - InitializeComponent(); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs deleted file mode 100644 index 411e8f735..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows.Media; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.Core.Services; -using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Screens.ProfileEditor.Conditions -{ - public class DataModelConditionListPredicateViewModel : DataModelConditionPredicateViewModel - { - private readonly IDataModelUIService _dataModelUIService; - - public DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate, - List modules, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IConditionOperatorService conditionOperatorService, - ISettingsService settingsService) - : base(dataModelConditionListPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) - { - _dataModelUIService = dataModelUIService; - LeftSideColor = new SolidColorBrush(Color.FromRgb(71, 108, 188)); - } - - public DataModelConditionListPredicate DataModelConditionListPredicate => (DataModelConditionListPredicate) Model; - - public override void Initialize() - { - base.Initialize(); - - DataModelPropertiesViewModel listDataModel = GetListDataModel(); - LeftSideSelectionViewModel.ChangeDataModel(listDataModel); - - // If this is a primitive list the user doesn't have much to choose, so preselect the list item for them - if (DataModelConditionListPredicate.DataModelConditionList.IsPrimitiveList && DataModelConditionListPredicate.LeftPath == null) - { - DataModelConditionListPredicate.UpdateLeftSide(listDataModel.Children.FirstOrDefault()?.DataModelPath); - Update(); - } - } - - public override void Evaluate() - { - - } - - public override void UpdateModules() - { - foreach (DataModelConditionViewModel dataModelConditionViewModel in Items) - dataModelConditionViewModel.UpdateModules(); - } - - protected override void OnInitialActivate() - { - base.OnInitialActivate(); - Initialize(); - } - - protected override List GetSupportedInputTypes() - { - IReadOnlyCollection editors = _dataModelUIService.RegisteredDataModelEditors; - List supportedInputTypes = editors.Select(e => e.SupportedType).ToList(); - supportedInputTypes.AddRange(editors.Where(e => e.CompatibleConversionTypes != null).SelectMany(e => e.CompatibleConversionTypes)); - - return supportedInputTypes; - } - - protected override Type GetLeftSideType() - { - return DataModelConditionListPredicate.DataModelConditionList.IsPrimitiveList - ? DataModelConditionListPredicate.DataModelConditionList.ListType - : LeftSideSelectionViewModel.DataModelPath?.GetPropertyType(); - } - - protected override List GetExtraRightSideDataModelViewModels() - { - if (GetListDataModel()?.Children?.FirstOrDefault() is DataModelPropertiesViewModel listValue) - return new List {listValue}; - return null; - } - - private DataModelPropertiesViewModel GetListDataModel() - { - ListPredicateWrapperDataModel wrapper = ListPredicateWrapperDataModel.Create( - DataModelConditionListPredicate.DataModelConditionList.ListType!, - DataModelConditionListPredicate.DataModelConditionList.ListPath?.GetPropertyDescription()?.ListItemName - ); - - return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(true)); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 4fe3a8c9b..a587da7cc 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -2,14 +2,13 @@ using System.Windows.Input; using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionsViewModel : Conductor, IProfileEditorPanelViewModel + public class DisplayConditionsViewModel : Screen, IProfileEditorPanelViewModel { private readonly INodeVmFactory _nodeVmFactory; private readonly IProfileEditorService _profileEditorService; @@ -103,12 +102,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions NotifyOfPropertyChange(nameof(AlwaysFinishTimeline)); NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled)); - if (renderProfileElement == null) - { - ActiveItem = null; - return; - } - RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml deleted file mode 100644 index 7fded19b2..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeView.xaml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs deleted file mode 100644 index 0b6d1eeef..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/ConditionalDataBindingModeViewModel.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using Artemis.Core; -using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.ConditionalDataBinding -{ - public sealed class ConditionalDataBindingModeViewModel : Conductor>.Collection.AllActive, - IDataBindingModeViewModel - { - private readonly IDataBindingsVmFactory _dataBindingsVmFactory; - private readonly IProfileEditorService _profileEditorService; - private bool _updating; - - public ConditionalDataBindingModeViewModel(ConditionalDataBinding conditionalDataBinding, - IProfileEditorService profileEditorService, - IDataBindingsVmFactory dataBindingsVmFactory) - { - _profileEditorService = profileEditorService; - _dataBindingsVmFactory = dataBindingsVmFactory; - - ConditionalDataBinding = conditionalDataBinding; - } - - public ConditionalDataBinding ConditionalDataBinding { get; } - - public void AddCondition() - { - DataBindingCondition condition = ConditionalDataBinding.AddCondition(); - - // Find the VM of the new condition - DataBindingConditionViewModel viewModel = Items.First(c => c.DataBindingCondition == condition); - viewModel.ActiveItem.AddCondition(); - - _profileEditorService.SaveSelectedProfileElement(); - } - - public void RemoveCondition(DataBindingCondition dataBindingCondition) - { - ConditionalDataBinding.RemoveCondition(dataBindingCondition); - } - - protected override void OnInitialActivate() - { - Initialize(); - base.OnInitialActivate(); - } - - private void UpdateItems() - { - _updating = true; - - // Remove old VMs - List> toRemove = Items.Where(c => !ConditionalDataBinding.Conditions.Contains(c.DataBindingCondition)).ToList(); - foreach (DataBindingConditionViewModel dataBindingConditionViewModel in toRemove) - { - Items.Remove(dataBindingConditionViewModel); - dataBindingConditionViewModel.Dispose(); - } - - // Add missing VMs - foreach (DataBindingCondition condition in ConditionalDataBinding.Conditions) - if (Items.All(c => c.DataBindingCondition != condition)) - Items.Add(_dataBindingsVmFactory.DataBindingConditionViewModel(condition)); - - // Fix order - ((BindableCollection>) Items).Sort(c => c.DataBindingCondition.Order); - - _updating = false; - } - - private void Initialize() - { - ConditionalDataBinding.ConditionsUpdated += ConditionalDataBindingOnConditionsUpdated; - Items.CollectionChanged += ItemsOnCollectionChanged; - UpdateItems(); - } - - private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_updating || e.Action != NotifyCollectionChangedAction.Add) - return; - - for (int index = 0; index < Items.Count; index++) - { - DataBindingConditionViewModel conditionViewModel = Items[index]; - conditionViewModel.DataBindingCondition.Order = index + 1; - } - - ConditionalDataBinding.ApplyOrder(); - - _profileEditorService.SaveSelectedProfileElement(); - } - - private void ConditionalDataBindingOnConditionsUpdated(object sender, EventArgs e) - { - UpdateItems(); - } - - public bool SupportsTestValue => false; - - public void Update() - { - UpdateItems(); - } - - public object GetTestValue() - { - throw new NotSupportedException(); - } - - #region IDisposable - - /// - public void Dispose() - { - ConditionalDataBinding.ConditionsUpdated -= ConditionalDataBindingOnConditionsUpdated; - - foreach (DataBindingConditionViewModel conditionViewModel in Items) - conditionViewModel.Dispose(); - Items.Clear(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml deleted file mode 100644 index 6ae8e1eeb..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionView.xaml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs deleted file mode 100644 index e9f620f21..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/ConditionalDataBinding/DataBindingConditionViewModel.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Conditions; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Input; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.ConditionalDataBinding -{ - public sealed class DataBindingConditionViewModel : Conductor, IDisposable - { - private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; - private readonly IDataModelUIService _dataModelUIService; - private readonly IProfileEditorService _profileEditorService; - - public DataBindingConditionViewModel(DataBindingCondition dataBindingCondition, - IProfileEditorService profileEditorService, - IDataModelConditionsVmFactory dataModelConditionsVmFactory, - IDataModelUIService dataModelUIService) - { - _profileEditorService = profileEditorService; - _dataModelConditionsVmFactory = dataModelConditionsVmFactory; - _dataModelUIService = dataModelUIService; - DataBindingCondition = dataBindingCondition; - } - - public DataBindingCondition DataBindingCondition { get; } - - public DataModelStaticViewModel ValueViewModel { get; set; } - - protected override void OnInitialActivate() - { - base.OnInitialActivate(); - List modules = new(); - if (_profileEditorService.SelectedProfileConfiguration?.Module != null) - modules.Add(_profileEditorService.SelectedProfileConfiguration.Module); - ActiveItem = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(DataBindingCondition.Condition, ConditionGroupType.General, modules); - ActiveItem.IsRootGroup = true; - - ActiveItem.Update(); - ActiveItem.Updated += ActiveItemOnUpdated; - - ValueViewModel = _dataModelUIService.GetStaticInputViewModel(typeof(TProperty), null); - ValueViewModel.ValueUpdated += ValueViewModelOnValueUpdated; - ValueViewModel.Value = DataBindingCondition.Value; - } - - private void ActiveItemOnUpdated(object sender, EventArgs e) - { - if (!ActiveItem.GetChildren().Any()) - DataBindingCondition.ConditionalDataBinding.RemoveCondition(DataBindingCondition); - } - - private void ValueViewModelOnValueUpdated(object sender, DataModelInputStaticEventArgs e) - { - DataBindingCondition.Value = (TProperty) Convert.ChangeType(e.Value, typeof(TProperty)); - _profileEditorService.SaveSelectedProfileElement(); - } - - public void AddCondition() - { - ((ConditionalDataBindingModeViewModel) Parent).AddCondition(); - } - - public void RemoveCondition() - { - ((ConditionalDataBindingModeViewModel)Parent).RemoveCondition(DataBindingCondition); - - } - - #region IDisposable - - /// - public void Dispose() - { - ValueViewModel?.Dispose(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml index 34fe61836..be5b8b20e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:s="https://github.com/canton7/Stylet" + xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> @@ -36,16 +37,8 @@ - - + Enable data binding + - - - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index 49b3040e9..9b0a82562 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -1,11 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; -using System.Timers; using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Entities.Profile.DataBindings; using Artemis.UI.Exceptions; -using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -13,63 +12,62 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { - public sealed class DataBindingViewModel : Conductor, IDataBindingViewModel + public sealed class DataBindingViewModel : Screen, IDataBindingViewModel { - private readonly IDataBindingsVmFactory _dataBindingsVmFactory; private readonly ICoreService _coreService; private readonly IProfileEditorService _profileEditorService; private int _easingTime; private bool _isDataBindingEnabled; private bool _isEasingTimeEnabled; - private DataBindingModeType _selectedDataBindingMode; + private DateTime _lastUpdate; private TimelineEasingViewModel _selectedEasingViewModel; private bool _updating; private bool _updatingTestResult; - private DateTime _lastUpdate; public DataBindingViewModel(DataBindingRegistration registration, ICoreService coreService, ISettingsService settingsService, IProfileEditorService profileEditorService, IDataModelUIService dataModelUIService, - IDataBindingsVmFactory dataBindingsVmFactory) + INodeService nodeService) { Registration = registration; _coreService = coreService; _profileEditorService = profileEditorService; - _dataBindingsVmFactory = dataBindingsVmFactory; DisplayName = Registration.DisplayName.ToUpper(); AlwaysApplyDataBindings = settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true); - DataBindingModes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(DataBindingModeType))); EasingViewModels = new BindableCollection(); + AvailableNodes = nodeService.AvailableNodes; TestInputValue = dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), null, true); TestResultValue = dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), null, true); } public DataBindingRegistration Registration { get; } + public NodeScript Script => Registration.DataBinding?.Script; + public PluginSetting AlwaysApplyDataBindings { get; } - public BindableCollection DataBindingModes { get; } public BindableCollection EasingViewModels { get; } + public IEnumerable AvailableNodes { get; } public DataModelDisplayViewModel TestInputValue { get; } public DataModelDisplayViewModel TestResultValue { get; } - public DataBindingModeType SelectedDataBindingMode - { - get => _selectedDataBindingMode; - set - { - if (!SetAndNotify(ref _selectedDataBindingMode, value)) return; - ApplyDataBindingMode(); - Update(); - } - } public bool IsDataBindingEnabled { get => _isDataBindingEnabled; - set => SetAndNotify(ref _isDataBindingEnabled, value); + set + { + SetAndNotify(ref _isDataBindingEnabled, value); + if (_updating) + return; + + if (value) + EnableDataBinding(); + else + DisableDataBinding(); + } } public TimelineEasingViewModel SelectedEasingViewModel @@ -102,162 +100,6 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings } } - protected override void OnInitialActivate() - { - Initialize(); - base.OnInitialActivate(); - } - - private void Initialize() - { - EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(v, false))); - - _lastUpdate = DateTime.Now; - _coreService.FrameRendered += OnFrameRendered; - CreateDataBindingModeModeViewModel(); - Update(); - } - - private void CreateDataBindingModeModeViewModel() - { - if (Registration.DataBinding?.DataBindingMode == null) - { - ActiveItem = null; - return; - } - - switch (Registration.DataBinding.DataBindingMode) - { - case DirectDataBinding directDataBinding: - ActiveItem = _dataBindingsVmFactory.DirectDataBindingModeViewModel(directDataBinding); - break; - case ConditionalDataBinding conditionalDataBinding: - ActiveItem = _dataBindingsVmFactory.ConditionalDataBindingModeViewModel(conditionalDataBinding); - break; - } - } - - private void Update() - { - if (_updating) - return; - - if (Registration.DataBinding == null) - { - IsDataBindingEnabled = false; - IsEasingTimeEnabled = false; - return; - } - - _updating = true; - - IsDataBindingEnabled = ActiveItem != null; - EasingTime = (int) Registration.DataBinding.EasingTime.TotalMilliseconds; - SelectedEasingViewModel = EasingViewModels.First(vm => vm.EasingFunction == Registration.DataBinding.EasingFunction); - IsEasingTimeEnabled = EasingTime > 0; - SelectedDataBindingMode = Registration.DataBinding.DataBindingMode switch - { - DirectDataBinding _ => DataBindingModeType.Direct, - ConditionalDataBinding _ => DataBindingModeType.Conditional, - _ => DataBindingModeType.None - }; - - ActiveItem?.Update(); - - _updating = false; - } - - private void ApplyChanges() - { - if (_updating) - return; - - if (Registration.DataBinding != null) - { - Registration.DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); - Registration.DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; - } - - _profileEditorService.SaveSelectedProfileElement(); - Update(); - } - - private void ApplyDataBindingMode() - { - if (_updating) - return; - - if (Registration.DataBinding != null && SelectedDataBindingMode == DataBindingModeType.None) - RemoveDataBinding(); - else - { - if (Registration.DataBinding == null && SelectedDataBindingMode != DataBindingModeType.None) - EnableDataBinding(); - - Registration.DataBinding!.ChangeDataBindingMode(SelectedDataBindingMode); - } - - CreateDataBindingModeModeViewModel(); - _profileEditorService.SaveSelectedProfileElement(); - } - - private void UpdateTestResult() - { - if (_updating || _updatingTestResult) - return; - - _updatingTestResult = true; - if (Registration.DataBinding == null || ActiveItem == null) - { - TestInputValue.UpdateValue(default); - TestResultValue.UpdateValue(default); - _updatingTestResult = false; - return; - } - - // If always update is disabled, do constantly update the data binding as long as the view model is open - // If always update is enabled, this is done for all data bindings in ProfileViewModel - if (!AlwaysApplyDataBindings.Value) - { - Registration.DataBinding.UpdateWithDelta(DateTime.Now - _lastUpdate); - _profileEditorService.UpdateProfilePreview(); - } - - if (ActiveItem.SupportsTestValue) - { - TProperty currentValue = Registration.Converter.ConvertFromObject(ActiveItem?.GetTestValue() ?? default(TProperty)); - TestInputValue.UpdateValue(currentValue); - TestResultValue.UpdateValue(Registration.DataBinding != null ? Registration.DataBinding.GetValue(currentValue) : default); - } - else - { - TestResultValue.UpdateValue(Registration.DataBinding != null ? Registration.DataBinding.GetValue(default) : default); - } - - _updatingTestResult = false; - } - - private void EnableDataBinding() - { - if (Registration.DataBinding != null) - return; - - Registration.LayerProperty.EnableDataBinding(Registration); - _profileEditorService.SaveSelectedProfileElement(); - } - - private void RemoveDataBinding() - { - if (Registration.DataBinding == null) - return; - - ActiveItem = null; - Registration.LayerProperty.DisableDataBinding(Registration.DataBinding); - Update(); - - _profileEditorService.SaveSelectedProfileElement(); - } - public void CopyDataBinding() { if (Registration.DataBinding != null) @@ -277,10 +119,115 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings Registration.DataBinding.EasingTime = dataBindingEntity.EasingTime; Registration.DataBinding.EasingFunction = (Easings.Functions) dataBindingEntity.EasingFunction; - Registration.DataBinding.ApplyDataBindingEntity(dataBindingEntity.DataBindingMode); - CreateDataBindingModeModeViewModel(); + + // TODO - Paste visual script + + Update(); + + _profileEditorService.SaveSelectedProfileElement(); + } + + protected override void OnInitialActivate() + { + Initialize(); + base.OnInitialActivate(); + } + + private void Initialize() + { + EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(v, false))); + + _lastUpdate = DateTime.Now; + _coreService.FrameRendered += OnFrameRendered; + Update(); + } + + private void Update() + { + if (_updating) + return; + + if (Registration.DataBinding == null) + { + IsDataBindingEnabled = false; + IsEasingTimeEnabled = false; + return; + } + + _updating = true; + + EasingTime = (int) Registration.DataBinding.EasingTime.TotalMilliseconds; + SelectedEasingViewModel = EasingViewModels.First(vm => vm.EasingFunction == Registration.DataBinding.EasingFunction); + IsDataBindingEnabled = true; + IsEasingTimeEnabled = EasingTime > 0; + + _updating = false; + } + + private void ApplyChanges() + { + if (_updating) + return; + + if (Registration.DataBinding != null) + { + Registration.DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); + Registration.DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; + } + + _profileEditorService.SaveSelectedProfileElement(); + Update(); + } + + private void UpdateTestResult() + { + if (_updating || _updatingTestResult) + return; + + _updatingTestResult = true; + + if (Registration.DataBinding == null) + { + TestInputValue.UpdateValue(default); + TestResultValue.UpdateValue(default); + _updatingTestResult = false; + return; + } + + // If always update is disabled, do constantly update the data binding as long as the view model is open + // If always update is enabled, this is done for all data bindings in ProfileViewModel + if (!AlwaysApplyDataBindings.Value) + { + Registration.DataBinding.UpdateWithDelta(DateTime.Now - _lastUpdate); + _profileEditorService.UpdateProfilePreview(); + } + + TProperty currentValue = Registration.Converter.ConvertFromObject(Registration.DataBinding.Script.Result ?? default(TProperty)); + TestInputValue.UpdateValue(currentValue); + TestResultValue.UpdateValue(Registration.DataBinding != null ? Registration.DataBinding.GetValue(currentValue) : default); + + _updatingTestResult = false; + } + + private void EnableDataBinding() + { + if (Registration.DataBinding != null) + return; + + Registration.LayerProperty.EnableDataBinding(Registration); + NotifyOfPropertyChange(nameof(Script)); + + _profileEditorService.SaveSelectedProfileElement(); + } + + private void DisableDataBinding() + { + if (Registration.DataBinding == null) + return; + + Registration.LayerProperty.DisableDataBinding(Registration.DataBinding); + NotifyOfPropertyChange(nameof(Script)); Update(); - _profileEditorService.SaveSelectedProfileElement(); } @@ -291,20 +238,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings _lastUpdate = DateTime.Now; } - private void OnUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) - { - UpdateTestResult(); - } - - #region IDisposable - /// public void Dispose() { _coreService.FrameRendered -= OnFrameRendered; } - - #endregion } public interface IDataBindingViewModel : IScreen, IDisposable diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml deleted file mode 100644 index 4d0635574..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml.cs deleted file mode 100644 index 86d341696..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierView.xaml.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding -{ - /// - /// Interaction logic for DataBindingModifierView.xaml - /// - public partial class DataBindingModifierView : UserControl - { - public DataBindingModifierView() - { - InitializeComponent(); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs deleted file mode 100644 index f6811db6f..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core; -using Artemis.Core.Services; -using Artemis.UI.Exceptions; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding.ModifierTypes; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Input; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding -{ - public sealed class DataBindingModifierViewModel : PropertyChangedBase, IDisposable - { - private readonly IDataBindingService _dataBindingService; - private readonly IDataModelUIService _dataModelUIService; - private readonly IProfileEditorService _profileEditorService; - private DataModelDynamicViewModel _dynamicSelectionViewModel; - private ModifierTypeCategoryViewModel _modifierTypeViewModels; - private BaseDataBindingModifierType _selectedModifierType; - private DataModelStaticViewModel _staticInputViewModel; - - public DataBindingModifierViewModel(DataBindingModifier modifier, - IDataBindingService dataBindingService, - ISettingsService settingsService, - IDataModelUIService dataModelUIService, - IProfileEditorService profileEditorService) - { - _dataBindingService = dataBindingService; - _dataModelUIService = dataModelUIService; - _profileEditorService = profileEditorService; - - ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues"); - - Modifier = modifier; - SelectModifierTypeCommand = new DelegateCommand(ExecuteSelectModifierTypeCommand); - - Update(); - } - - public DelegateCommand SelectModifierTypeCommand { get; } - public PluginSetting ShowDataModelValues { get; } - public DataBindingModifier Modifier { get; } - - public ModifierTypeCategoryViewModel ModifierTypeViewModels - { - get => _modifierTypeViewModels; - set => SetAndNotify(ref _modifierTypeViewModels, value); - } - - public BaseDataBindingModifierType SelectedModifierType - { - get => _selectedModifierType; - set => SetAndNotify(ref _selectedModifierType, value); - } - - public DataModelDynamicViewModel DynamicSelectionViewModel - { - get => _dynamicSelectionViewModel; - private set => SetAndNotify(ref _dynamicSelectionViewModel, value); - } - - public DataModelStaticViewModel StaticInputViewModel - { - get => _staticInputViewModel; - private set => SetAndNotify(ref _staticInputViewModel, value); - } - - public void Delete() - { - Modifier.DirectDataBinding.RemoveModifier(Modifier); - _profileEditorService.SaveSelectedProfileElement(); - } - - private void ParameterSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) - { - Modifier.UpdateParameterDynamic(e.DataModelPath); - _profileEditorService.SaveSelectedProfileElement(); - } - - private void StaticInputViewModelOnValueUpdated(object sender, DataModelInputStaticEventArgs e) - { - Modifier.UpdateParameterStatic(e.Value); - _profileEditorService.SaveSelectedProfileElement(); - } - - private void Update() - { - Type sourceType = Modifier.DirectDataBinding.GetSourceType(); - if (sourceType == null) - throw new ArtemisUIException("Cannot use a data binding modifier VM for a data binding without a source"); - - if (Modifier.ModifierType == null || Modifier.ModifierType.ParameterType == null) - { - DisposeDynamicSelectionViewModel(); - DisposeStaticInputViewModel(); - } - else if (Modifier.ParameterType == ProfileRightSideType.Dynamic) - { - DisposeStaticInputViewModel(); - DynamicSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.SelectedProfileConfiguration.Module); - if (DynamicSelectionViewModel != null) - { - DynamicSelectionViewModel.DisplaySwitchButton = true; - DynamicSelectionViewModel.PropertySelected += ParameterSelectionViewModelOnPropertySelected; - DynamicSelectionViewModel.SwitchToStaticRequested += DynamicSelectionViewModelOnSwitchToStaticRequested; - DynamicSelectionViewModel.FilterTypes = new[] {Modifier.ModifierType.ParameterType ?? sourceType}; - } - } - else - { - DisposeDynamicSelectionViewModel(); - StaticInputViewModel = _dataModelUIService.GetStaticInputViewModel(Modifier.ModifierType.ParameterType ?? sourceType, null); - if (StaticInputViewModel != null) - { - StaticInputViewModel.DisplaySwitchButton = true; - StaticInputViewModel.ValueUpdated += StaticInputViewModelOnValueUpdated; - StaticInputViewModel.SwitchToDynamicRequested += StaticInputViewModelOnSwitchToDynamicRequested; - } - } - - // Modifier type - ModifierTypeCategoryViewModel root = new(null, null); - IEnumerable> modifierTypes = _dataBindingService.GetCompatibleModifierTypes(sourceType, ModifierTypePart.Value).GroupBy(t => t.Category); - foreach (IGrouping dataBindingModifierTypes in modifierTypes) - { - IEnumerable viewModels = dataBindingModifierTypes.Select(t => new ModifierTypeViewModel(t)); - if (dataBindingModifierTypes.Key == null) - root.Children.AddRange(viewModels); - else - root.Children.Add(new ModifierTypeCategoryViewModel(dataBindingModifierTypes.Key, viewModels)); - } - - ModifierTypeViewModels = root; - SelectedModifierType = Modifier.ModifierType; - - // Parameter - if (DynamicSelectionViewModel != null) - DynamicSelectionViewModel.ChangeDataModelPath(Modifier.ParameterPath); - else if (StaticInputViewModel != null) - StaticInputViewModel.Value = Modifier.ParameterStaticValue; - } - - private void ExecuteSelectModifierTypeCommand(object context) - { - if (!(context is ModifierTypeViewModel modifierTypeViewModel)) - return; - - Modifier.UpdateModifierType(modifierTypeViewModel.ModifierType); - _profileEditorService.SaveSelectedProfileElement(); - - Update(); - } - - #region IDisposable - - /// - public void Dispose() - { - DisposeDynamicSelectionViewModel(); - DisposeStaticInputViewModel(); - } - - private void DisposeStaticInputViewModel() - { - if (StaticInputViewModel != null) - { - StaticInputViewModel.Dispose(); - StaticInputViewModel.ValueUpdated -= StaticInputViewModelOnValueUpdated; - StaticInputViewModel.SwitchToDynamicRequested -= StaticInputViewModelOnSwitchToDynamicRequested; - StaticInputViewModel = null; - } - } - - private void DisposeDynamicSelectionViewModel() - { - if (DynamicSelectionViewModel != null) - { - DynamicSelectionViewModel.Dispose(); - DynamicSelectionViewModel.PropertySelected -= ParameterSelectionViewModelOnPropertySelected; - DynamicSelectionViewModel.SwitchToStaticRequested -= DynamicSelectionViewModelOnSwitchToStaticRequested; - DynamicSelectionViewModel = null; - } - } - - #endregion - - #region Event handlers - - private void DynamicSelectionViewModelOnSwitchToStaticRequested(object sender, EventArgs e) - { - Modifier.ParameterType = ProfileRightSideType.Static; - - // Ensure the right static value is never null when the preferred type is a value type - if (SelectedModifierType.ParameterType != null && - SelectedModifierType.ParameterType.IsValueType && Modifier.ParameterStaticValue == null) - Modifier.UpdateParameterStatic(SelectedModifierType.ParameterType.GetDefault()); - - Update(); - } - - private void StaticInputViewModelOnSwitchToDynamicRequested(object sender, EventArgs e) - { - Modifier.ParameterType = ProfileRightSideType.Dynamic; - Update(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeView.xaml deleted file mode 100644 index d33ce960e..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeView.xaml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs deleted file mode 100644 index 40479a351..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DirectDataBindingModeViewModel.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.UI.Extensions; -using Artemis.UI.Ninject.Factories; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Input; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding -{ - public sealed class DirectDataBindingModeViewModel : Screen, IDataBindingModeViewModel - { - private readonly IDataBindingsVmFactory _dataBindingsVmFactory; - private readonly IDataModelUIService _dataModelUIService; - private readonly IProfileEditorService _profileEditorService; - private bool _canAddModifier; - private DataModelDynamicViewModel _targetSelectionViewModel; - private bool _updating; - - public DirectDataBindingModeViewModel(DirectDataBinding directDataBinding, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - IDataBindingsVmFactory dataBindingsVmFactory) - { - _profileEditorService = profileEditorService; - _dataModelUIService = dataModelUIService; - _dataBindingsVmFactory = dataBindingsVmFactory; - - DirectDataBinding = directDataBinding; - ModifierViewModels = new BindableCollection>(); - - Initialize(); - } - - - public DirectDataBinding DirectDataBinding { get; } - public BindableCollection> ModifierViewModels { get; } - - public bool CanAddModifier - { - get => _canAddModifier; - set => SetAndNotify(ref _canAddModifier, value); - } - - public DataModelDynamicViewModel TargetSelectionViewModel - { - get => _targetSelectionViewModel; - private set => SetAndNotify(ref _targetSelectionViewModel, value); - } - - public bool SupportsTestValue => true; - - public void Update() - { - TargetSelectionViewModel.ChangeDataModelPath(DirectDataBinding.SourcePath); - TargetSelectionViewModel.FilterTypes = new[] {DirectDataBinding.DataBinding.GetTargetType()}; - - CanAddModifier = DirectDataBinding.SourcePath != null && DirectDataBinding.SourcePath.IsValid; - UpdateModifierViewModels(); - } - - public object GetTestValue() - { - return DirectDataBinding.SourcePath?.GetValue(); - } - - #region IDisposable - - /// - public void Dispose() - { - TargetSelectionViewModel.PropertySelected -= TargetSelectionViewModelOnPropertySelected; - TargetSelectionViewModel.Dispose(); - DirectDataBinding.ModifiersUpdated -= DirectDataBindingOnModifiersUpdated; - - foreach (DataBindingModifierViewModel dataBindingModifierViewModel in ModifierViewModels) - dataBindingModifierViewModel.Dispose(); - ModifierViewModels.Clear(); - } - - #endregion - - private void Initialize() - { - DirectDataBinding.ModifiersUpdated += DirectDataBindingOnModifiersUpdated; - TargetSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.SelectedProfileConfiguration.Module); - TargetSelectionViewModel.PropertySelected += TargetSelectionViewModelOnPropertySelected; - ModifierViewModels.CollectionChanged += ModifierViewModelsOnCollectionChanged; - Update(); - } - - private void ModifierViewModelsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (_updating || e.Action != NotifyCollectionChangedAction.Add) - return; - - for (int index = 0; index < ModifierViewModels.Count; index++) - { - DataBindingModifierViewModel dataBindingModifierViewModel = ModifierViewModels[index]; - dataBindingModifierViewModel.Modifier.Order = index + 1; - } - - DirectDataBinding.ApplyOrder(); - - _profileEditorService.SaveSelectedProfileElement(); - } - - #region Target - - private void TargetSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) - { - DirectDataBinding.UpdateSource(e.DataModelPath); - Update(); - - _profileEditorService.SaveSelectedProfileElement(); - } - - #endregion - - #region Modifiers - - public void AddModifier() - { - DirectDataBinding.AddModifier(ProfileRightSideType.Dynamic); - _profileEditorService.SaveSelectedProfileElement(); - } - - private void UpdateModifierViewModels() - { - _updating = true; - - // Remove old VMs - List> toRemove = ModifierViewModels.Where(m => !DirectDataBinding.Modifiers.Contains(m.Modifier)).ToList(); - foreach (DataBindingModifierViewModel modifierViewModel in toRemove) - { - ModifierViewModels.Remove(modifierViewModel); - modifierViewModel.Dispose(); - } - - // Don't create children without a source or with an invalid source - if (DirectDataBinding.SourcePath == null || !DirectDataBinding.SourcePath.IsValid) - return; - - // Add missing VMs - foreach (DataBindingModifier modifier in DirectDataBinding.Modifiers) - { - if (ModifierViewModels.All(m => m.Modifier != modifier)) - ModifierViewModels.Add(_dataBindingsVmFactory.DataBindingModifierViewModel(modifier)); - } - - // Fix order - ModifierViewModels.Sort(m => m.Modifier.Order); - - _updating = false; - } - - private void DirectDataBindingOnModifiersUpdated(object sender, EventArgs e) - { - UpdateModifierViewModels(); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/IModifierTypeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/IModifierTypeViewModel.cs deleted file mode 100644 index baa398a12..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/IModifierTypeViewModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding.ModifierTypes -{ - public interface IModifierTypeViewModel - { - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/ModifierTypeCategoryViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/ModifierTypeCategoryViewModel.cs deleted file mode 100644 index e1d5e3966..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/ModifierTypeCategoryViewModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding.ModifierTypes -{ - public class ModifierTypeCategoryViewModel : IModifierTypeViewModel - { - public ModifierTypeCategoryViewModel(string category, IEnumerable children) - { - Category = category; - Children = children == null - ? new BindableCollection() - : new BindableCollection(children); - } - - public string Category { get; set; } - public BindableCollection Children { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/ModifierTypeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/ModifierTypeViewModel.cs deleted file mode 100644 index af7eb7daf..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/ModifierTypes/ModifierTypeViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Artemis.Core; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBinding.ModifierTypes -{ - public class ModifierTypeViewModel : IModifierTypeViewModel - { - public ModifierTypeViewModel(BaseDataBindingModifierType modifierType) - { - ModifierType = modifierType; - } - - public BaseDataBindingModifierType ModifierType { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/IDataBindingModeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/IDataBindingModeViewModel.cs deleted file mode 100644 index 09db1fe3f..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/IDataBindingModeViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings -{ - public interface IDataBindingModeViewModel : IScreen, IDisposable - { - bool SupportsTestValue { get; } - void Update(); - object GetTestValue(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs index bbfb771cd..dc27af8a1 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs @@ -3,14 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using System.Windows; -using System.Windows.Media; -using System.Windows.Media.Imaging; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using FluentValidation; @@ -22,7 +18,6 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit { public class ProfileEditViewModel : DialogViewModelBase { - private readonly DataModelConditionGroup _dataModelConditionGroup; private readonly List _modules; private readonly IProfileService _profileService; private bool _changedImage; @@ -37,14 +32,12 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit IProfileService profileService, IPluginManagementService pluginManagementService, ISidebarVmFactory sidebarVmFactory, - IDataModelConditionsVmFactory dataModelConditionsVmFactory, IModelValidator validator) : base(validator) { ProfileConfiguration = profileConfiguration; IsNew = isNew; _profileService = profileService; - _dataModelConditionGroup = ProfileConfiguration.ActivationCondition ?? new DataModelConditionGroup(null); _modules = ProfileConfiguration.Module != null ? new List {ProfileConfiguration.Module} : new List(); IconTypes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(ProfileConfigurationIconType))); @@ -54,10 +47,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit pluginManagementService.GetFeaturesOfType().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m)) ); Initializing = true; - - ActivationConditionViewModel = dataModelConditionsVmFactory.DataModelConditionGroupViewModel(_dataModelConditionGroup, ConditionGroupType.General, _modules); - ActivationConditionViewModel.ConductWith(this); - ActivationConditionViewModel.IsRootGroup = true; + ModuleActivationRequirementsViewModel = new ModuleActivationRequirementsViewModel(sidebarVmFactory); ModuleActivationRequirementsViewModel.ConductWith(this); ModuleActivationRequirementsViewModel.SetModule(ProfileConfiguration.Module); @@ -84,8 +74,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit Initializing = false; }); } - - public DataModelConditionGroupViewModel ActivationConditionViewModel { get; } + public ModuleActivationRequirementsViewModel ModuleActivationRequirementsViewModel { get; } public ProfileConfigurationHotkeyViewModel EnableHotkeyViewModel { get; } public ProfileConfigurationHotkeyViewModel DisableHotkeyViewModel { get; } @@ -160,7 +149,6 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit if (value != null) _modules.Add(value.Module); - ActivationConditionViewModel.UpdateModules(); ModuleActivationRequirementsViewModel.SetModule(value?.Module); } } @@ -184,9 +172,6 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit ProfileConfiguration.Module = SelectedModule?.Module; - if (_dataModelConditionGroup.Children.Any()) - ProfileConfiguration.ActivationCondition = _dataModelConditionGroup; - if (_changedImage) { ProfileConfiguration.Icon.FileIcon = SelectedImage; diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index bf5de01e7..93f3b3450 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -50,5 +50,8 @@ $(DefaultXamlRuntime) Designer + + $(DefaultXamlRuntime) + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs index 4487ba42c..85f6a8a7c 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs @@ -1,9 +1,12 @@ using System; +using System.Collections; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; +using System.Windows.Media; using System.Windows.Shapes; +using Artemis.Core; using Artemis.VisualScripting.Editor.Controls.Wrapper; namespace Artemis.VisualScripting.Editor.Controls @@ -48,7 +51,7 @@ namespace Artemis.VisualScripting.Editor.Controls public Point ValuePosition { - get => (Point) GetValue(ValuePositionProperty); + get => (Point)GetValue(ValuePositionProperty); set => SetValue(ValuePositionProperty, value); } @@ -60,7 +63,7 @@ namespace Artemis.VisualScripting.Editor.Controls { _path = GetTemplateChild(PART_PATH) as Path ?? throw new NullReferenceException($"The Path '{PART_PATH}' is missing."); _path.MouseDown += OnPathMouseDown; - + Unloaded += OnUnloaded; } @@ -104,25 +107,31 @@ namespace Artemis.VisualScripting.Editor.Controls newCable.To.PropertyChanged += OnPinPropertyChanged; } + UpdateBorderBrush(); UpdateValuePosition(); } - + private void OnPinPropertyChanged(object sender, PropertyChangedEventArgs e) { - UpdateValuePosition(); + if (e.PropertyName == nameof(VisualScriptPin.AbsolutePosition)) + UpdateValuePosition(); + } + + private void UpdateBorderBrush() + { + // if (Cable.From.Pin.Type.IsAssignableTo(typeof(IList))) + // BorderBrush = new SolidColorBrush(Colors.MediumPurple); } private void UpdateValuePosition() { if (Cable.From == null || Cable.To == null) - ValuePosition = new Point(); - else - { - ValuePosition = new Point( - Cable.From.AbsolutePosition.X + ((Cable.To.AbsolutePosition.X - Cable.From.AbsolutePosition.X) / 2), - Cable.From.AbsolutePosition.Y + ((Cable.To.AbsolutePosition.Y - Cable.From.AbsolutePosition.Y) / 2) - ); - } + return; + + ValuePosition = new Point( + Cable.From.AbsolutePosition.X + ((Cable.To.AbsolutePosition.X - Cable.From.AbsolutePosition.X) / 2), + Cable.From.AbsolutePosition.Y + ((Cable.To.AbsolutePosition.Y - Cable.From.AbsolutePosition.Y) / 2) + ); } #endregion diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml index e5cb83c06..0a507021d 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls" - xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters"> + xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters" + xmlns:collections="clr-namespace:System.Collections;assembly=System.Runtime"> + + + + diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml index 506619a99..afa908aa7 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml @@ -7,14 +7,14 @@ - @@ -27,14 +27,14 @@ Height="{Binding SurfaceSize, RelativeSource={RelativeSource TemplatedParent}}" Background="{TemplateBinding Background}"> - - @@ -50,10 +50,10 @@ @@ -79,7 +79,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.sln.DotSettings b/src/Artemis.sln.DotSettings index c899abc86..7af04acad 100644 --- a/src/Artemis.sln.DotSettings +++ b/src/Artemis.sln.DotSettings @@ -2,6 +2,7 @@ Inherit True True + True ERROR ERROR Built-in: Full Cleanup From b8077ca589ea115018d9cab891f672f83bced47f Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 19 Aug 2021 11:40:50 +0200 Subject: [PATCH 012/270] Nodes - Added type color registration system Nodes - Generate (stable) type colors for undefined types --- .../Extensions/SKColorExtensions.cs | 17 ++- src/Artemis.Core/Services/NodeService.cs | 50 +++++-- src/Artemis.Core/Stores/NodeTypeStore.cs | 61 ++++++-- .../Registrations/NodeTypeRegistration.cs | 55 ++++++- .../Services/ProfileEditorService.cs | 136 +++++++++--------- .../Utilities/TypeUtilities.cs | 43 ++++++ .../DisplayConditionsViewModel.cs | 6 +- .../Services/RegistrationService.cs | 9 ++ .../Controls/VisualScriptCablePresenter.cs | 13 +- .../Controls/VisualScriptPinPresenter.cs | 9 ++ .../Styles/VisualScriptCablePresenter.xaml | 20 --- .../Styles/VisualScriptPinPresenter.xaml | 25 ---- 12 files changed, 297 insertions(+), 147 deletions(-) create mode 100644 src/Artemis.UI.Shared/Utilities/TypeUtilities.cs diff --git a/src/Artemis.Core/Extensions/SKColorExtensions.cs b/src/Artemis.Core/Extensions/SKColorExtensions.cs index dbc14239f..f1e9297ec 100644 --- a/src/Artemis.Core/Extensions/SKColorExtensions.cs +++ b/src/Artemis.Core/Extensions/SKColorExtensions.cs @@ -16,7 +16,7 @@ namespace Artemis.Core /// The RGB.NET color public static Color ToRgbColor(this SKColor color) { - return new(color.Alpha, color.Red, color.Green, color.Blue); + return new Color(color.Alpha, color.Red, color.Green, color.Blue); } /// @@ -49,7 +49,7 @@ namespace Artemis.Core /// The sum of the two colors public static SKColor Sum(this SKColor a, SKColor b) { - return new( + return new SKColor( ClampToByte(a.Red + b.Red), ClampToByte(a.Green + b.Green), ClampToByte(a.Blue + b.Blue), @@ -57,6 +57,19 @@ namespace Artemis.Core ); } + /// + /// Darkens the color by the specified amount + /// + /// The color to darken + /// The brightness of the new color + /// The darkened color + public static SKColor Darken(this SKColor c, float amount) + { + c.ToHsl(out float h, out float s, out float l); + l *= 1f - amount; + return SKColor.FromHsl(h, s, l); + } + private static byte ClampToByte(float value) { return (byte) Math.Clamp(value, 0, 255); diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 74d856f51..66ffd7804 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -4,24 +4,19 @@ using System.Reflection; using Artemis.Storage.Entities.Profile.Nodes; using Ninject; using Ninject.Parameters; +using SkiaSharp; namespace Artemis.Core.Services { internal class NodeService : INodeService { - private readonly IKernel _kernel; - #region Constants private static readonly Type TYPE_NODE = typeof(INode); #endregion - #region Properties & Fields - - public IEnumerable AvailableNodes => NodeTypeStore.GetAll(); - - #endregion + private readonly IKernel _kernel; #region Constructors @@ -32,8 +27,20 @@ namespace Artemis.Core.Services #endregion + #region Properties & Fields + + public IEnumerable AvailableNodes => NodeTypeStore.GetAll(); + + #endregion + #region Methods + /// + public TypeColorRegistration? GetTypeColor(Type type) + { + return NodeTypeStore.GetColor(type); + } + public NodeTypeRegistration RegisterNodeType(Plugin plugin, Type nodeType) { if (plugin == null) throw new ArgumentNullException(nameof(plugin)); @@ -46,10 +53,18 @@ namespace Artemis.Core.Services string description = nodeAttribute?.Description ?? string.Empty; string category = nodeAttribute?.Category ?? string.Empty; - NodeData nodeData = new(plugin, nodeType, name, description, category, (e) => CreateNode(e, nodeType)); + NodeData nodeData = new(plugin, nodeType, name, description, category, e => CreateNode(e, nodeType)); return NodeTypeStore.Add(nodeData); } + public TypeColorRegistration RegisterTypeColor(Plugin plugin, Type type, SKColor color) + { + if (plugin == null) throw new ArgumentNullException(nameof(plugin)); + if (type == null) throw new ArgumentNullException(nameof(type)); + + return NodeTypeStore.AddColor(type, color, plugin); + } + private INode CreateNode(NodeEntity? entity, Type nodeType) { INode node = _kernel.Get(nodeType) as INode ?? throw new InvalidOperationException($"Node {nodeType} is not an INode"); @@ -72,20 +87,33 @@ namespace Artemis.Core.Services } /// - /// A service that provides access to the node system + /// A service that provides access to the node system /// public interface INodeService : IArtemisService { /// - /// Gets all available nodes + /// Gets all available nodes /// IEnumerable AvailableNodes { get; } /// - /// Initializes a node of the provided + /// Gets the best matching registration for the provided type + /// + TypeColorRegistration? GetTypeColor(Type type); + + /// + /// Registers a node of the provided /// /// The plugin the node belongs to /// The type of node to initialize NodeTypeRegistration RegisterNodeType(Plugin plugin, Type nodeType); + + /// + /// Registers a type with a provided color for use in the node editor + /// + /// The plugin making the registration + /// The type to associate the color with + /// The color to display + TypeColorRegistration RegisterTypeColor(Plugin plugin, Type type, SKColor color); } } \ No newline at end of file diff --git a/src/Artemis.Core/Stores/NodeTypeStore.cs b/src/Artemis.Core/Stores/NodeTypeStore.cs index 705a4d223..aee2896e3 100644 --- a/src/Artemis.Core/Stores/NodeTypeStore.cs +++ b/src/Artemis.Core/Stores/NodeTypeStore.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using SkiaSharp; namespace Artemis.Core { internal class NodeTypeStore { private static readonly List Registrations = new(); + private static readonly List ColorRegistrations = new(); public static NodeTypeRegistration Add(NodeData nodeData) { @@ -19,7 +21,7 @@ namespace Artemis.Core if (Registrations.Any(r => r.NodeData == nodeData)) throw new ArtemisCoreException($"Data binding modifier type store already contains modifier '{nodeData.Name}'"); - typeRegistration = new NodeTypeRegistration(nodeData, nodeData.Plugin) { IsInStore = true }; + typeRegistration = new NodeTypeRegistration(nodeData, nodeData.Plugin) {IsInStore = true}; Registrations.Add(typeRegistration); } @@ -32,7 +34,7 @@ namespace Artemis.Core lock (Registrations) { if (!Registrations.Contains(typeRegistration)) - throw new ArtemisCoreException($"Data binding modifier type store does not contain modifier type '{typeRegistration.NodeData.Name}'"); + throw new ArtemisCoreException($"Node type store does not contain modifier type '{typeRegistration.NodeData.Name}'"); Registrations.Remove(typeRegistration); typeRegistration.IsInStore = false; @@ -57,6 +59,51 @@ namespace Artemis.Core } } + public static Plugin? GetPlugin(INode node) + { + lock (Registrations) + { + return Registrations.FirstOrDefault(r => r.Plugin.GetType().Assembly == node.GetType().Assembly)?.Plugin; + } + } + + public static TypeColorRegistration AddColor(Type type, SKColor color, Plugin plugin) + { + TypeColorRegistration typeColorRegistration; + lock (ColorRegistrations) + { + if (ColorRegistrations.Any(r => r.Type == type)) + throw new ArtemisCoreException($"Node color store already contains a color for '{type.Name}'"); + + typeColorRegistration = new TypeColorRegistration(type, color, plugin) {IsInStore = true}; + ColorRegistrations.Add(typeColorRegistration); + } + + return typeColorRegistration; + } + + public static void RemoveColor(TypeColorRegistration typeColorRegistration) + { + lock (ColorRegistrations) + { + if (!ColorRegistrations.Contains(typeColorRegistration)) + throw new ArtemisCoreException($"Node color store does not contain modifier type '{typeColorRegistration.Type.Name}'"); + + ColorRegistrations.Remove(typeColorRegistration); + typeColorRegistration.IsInStore = false; + } + } + + public static TypeColorRegistration? GetColor(Type type) + { + lock (ColorRegistrations) + { + return ColorRegistrations + .OrderByDescending(r => r.Type.ScoreCastability(type)) + .FirstOrDefault(r => r.Type.ScoreCastability(type) > 0); + } + } + #region Events public static event EventHandler? NodeTypeAdded; @@ -73,13 +120,5 @@ namespace Artemis.Core } #endregion - - public static Plugin? GetPlugin(INode node) - { - lock (Registrations) - { - return Registrations.FirstOrDefault(r => r.Plugin.GetType().Assembly == node.GetType().Assembly)?.Plugin; - } - } } -} +} \ No newline at end of file diff --git a/src/Artemis.Core/Stores/Registrations/NodeTypeRegistration.cs b/src/Artemis.Core/Stores/Registrations/NodeTypeRegistration.cs index a9b8e4873..799bee80e 100644 --- a/src/Artemis.Core/Stores/Registrations/NodeTypeRegistration.cs +++ b/src/Artemis.Core/Stores/Registrations/NodeTypeRegistration.cs @@ -1,9 +1,10 @@ using System; +using SkiaSharp; namespace Artemis.Core { /// - /// Represents a registration for a type of + /// Represents a registration for a type of /// public class NodeTypeRegistration { @@ -15,13 +16,16 @@ namespace Artemis.Core Plugin.Disabled += OnDisabled; } + /// + /// Gets the node data that was registered + /// public NodeData NodeData { get; } /// /// Gets the plugin the node is associated with /// public Plugin Plugin { get; } - + /// /// Gets a boolean indicating whether the registration is in the internal Core store /// @@ -34,4 +38,51 @@ namespace Artemis.Core NodeTypeStore.Remove(this); } } + + /// + /// Represents a registration for a to associate with a certain + /// + public class TypeColorRegistration + { + internal TypeColorRegistration(Type type, SKColor color, Plugin plugin) + { + Type = type; + Color = color; + Plugin = plugin; + + Plugin.Disabled += OnDisabled; + } + + /// + /// Gets the type + /// + public Type Type { get; } + + /// + /// Gets the color + /// + public SKColor Color { get; } + + /// + /// Gets a darkened tone of the + /// + public SKColor DarkenedColor => Color.Darken(0.35f); + + /// + /// Gets the plugin type color is associated with + /// + public Plugin Plugin { get; } + + /// + /// Gets a boolean indicating whether the registration is in the internal Core store + /// + public bool IsInStore { get; internal set; } + + private void OnDisabled(object? sender, EventArgs e) + { + Plugin.Disabled -= OnDisabled; + if (IsInStore) + NodeTypeStore.RemoveColor(this); + } + } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index f3f3626ea..37cccd627 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -20,10 +20,10 @@ namespace Artemis.UI.Shared.Services { private readonly IKernel _kernel; private readonly ILogger _logger; + private readonly IModuleService _moduleService; private readonly IProfileService _profileService; private readonly List _registeredPropertyEditors; private readonly IRgbService _rgbService; - private readonly IModuleService _moduleService; private readonly object _selectedProfileElementLock = new(); private readonly object _selectedProfileLock = new(); private TimeSpan _currentTime; @@ -31,7 +31,8 @@ namespace Artemis.UI.Shared.Services private int _pixelsPerSecond; private bool _suspendEditing; - public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService, IModuleService moduleService) + public ProfileEditorService(IKernel kernel, ILogger logger, ICoreService coreService, IProfileService profileService, IRgbService rgbService, IModuleService moduleService, + INodeService nodeService) { _kernel = kernel; _logger = logger; @@ -40,9 +41,56 @@ namespace Artemis.UI.Shared.Services _moduleService = moduleService; _registeredPropertyEditors = new List(); coreService.FrameRendered += CoreServiceOnFrameRendered; + + TypeUtilities.NodeService = nodeService; PixelsPerSecond = 100; } + protected virtual void OnSelectedProfileChanged(ProfileConfigurationEventArgs e) + { + SelectedProfileChanged?.Invoke(this, e); + } + + protected virtual void OnSelectedProfileUpdated(ProfileConfigurationEventArgs e) + { + SelectedProfileSaved?.Invoke(this, e); + } + + protected virtual void OnSelectedProfileElementChanged(RenderProfileElementEventArgs e) + { + SelectedProfileElementChanged?.Invoke(this, e); + } + + protected virtual void OnSelectedProfileElementUpdated(RenderProfileElementEventArgs e) + { + SelectedProfileElementSaved?.Invoke(this, e); + } + + protected virtual void OnCurrentTimeChanged() + { + CurrentTimeChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnPixelsPerSecondChanged() + { + PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSuspendEditingChanged() + { + SuspendEditingChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnProfilePreviewUpdated() + { + ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnSelectedDataBindingChanged() + { + SelectedDataBindingChanged?.Invoke(this, EventArgs.Empty); + } + private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) { if (!_doTick) return; @@ -89,7 +137,9 @@ namespace Artemis.UI.Shared.Services return; if (renderElement.Suspended) + { renderElement.Disable(); + } else { renderElement.Enable(); @@ -104,7 +154,6 @@ namespace Artemis.UI.Shared.Services } public ReadOnlyCollection RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly(); - public bool Playing { get; set; } public bool SuspendEditing @@ -183,10 +232,10 @@ namespace Artemis.UI.Shared.Services // No need to deactivate the profile, if needed it will be deactivated next update if (SelectedProfileConfiguration != null) SelectedProfileConfiguration.IsBeingEdited = false; - + PreviousSelectedProfileConfiguration = SelectedProfileConfiguration; SelectedProfileConfiguration = profileConfiguration; - + // The new profile may need activation if (SelectedProfileConfiguration != null) { @@ -319,10 +368,8 @@ namespace Artemis.UI.Shared.Services if (existing != null) { if (existing.Plugin != plugin) - { throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " + $"registered by {existing.Plugin}"); - } return existing; } @@ -367,10 +414,8 @@ namespace Artemis.UI.Shared.Services if (snapToCurrentTime) // Snap to the current time - { if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds) return CurrentTime; - } if (snapTimes != null) { @@ -406,9 +451,13 @@ namespace Artemis.UI.Shared.Services viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments); } else if (registration != null) + { viewModelType = registration.ViewModelType; + } else + { return null; + } if (viewModelType == null) return null; @@ -428,6 +477,16 @@ namespace Artemis.UI.Shared.Services .ToList(); } + public event EventHandler? SelectedProfileChanged; + public event EventHandler? SelectedProfileSaved; + public event EventHandler? SelectedProfileElementChanged; + public event EventHandler? SelectedProfileElementSaved; + public event EventHandler? SelectedDataBindingChanged; + public event EventHandler? CurrentTimeChanged; + public event EventHandler? PixelsPerSecondChanged; + public event EventHandler? SuspendEditingChanged; + public event EventHandler? ProfilePreviewUpdated; + #region Copy/paste public ProfileElement? DuplicateProfileElement(ProfileElement profileElement) @@ -507,64 +566,5 @@ namespace Artemis.UI.Shared.Services } #endregion - - #region Events - - public event EventHandler? SelectedProfileChanged; - public event EventHandler? SelectedProfileSaved; - public event EventHandler? SelectedProfileElementChanged; - public event EventHandler? SelectedProfileElementSaved; - public event EventHandler? SelectedDataBindingChanged; - public event EventHandler? CurrentTimeChanged; - public event EventHandler? PixelsPerSecondChanged; - public event EventHandler? SuspendEditingChanged; - public event EventHandler? ProfilePreviewUpdated; - - protected virtual void OnSelectedProfileChanged(ProfileConfigurationEventArgs e) - { - SelectedProfileChanged?.Invoke(this, e); - } - - protected virtual void OnSelectedProfileUpdated(ProfileConfigurationEventArgs e) - { - SelectedProfileSaved?.Invoke(this, e); - } - - protected virtual void OnSelectedProfileElementChanged(RenderProfileElementEventArgs e) - { - SelectedProfileElementChanged?.Invoke(this, e); - } - - protected virtual void OnSelectedProfileElementUpdated(RenderProfileElementEventArgs e) - { - SelectedProfileElementSaved?.Invoke(this, e); - } - - protected virtual void OnCurrentTimeChanged() - { - CurrentTimeChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnPixelsPerSecondChanged() - { - PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnSuspendEditingChanged() - { - SuspendEditingChanged?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnProfilePreviewUpdated() - { - ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty); - } - - protected virtual void OnSelectedDataBindingChanged() - { - SelectedDataBindingChanged?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs b/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs new file mode 100644 index 000000000..b979006e4 --- /dev/null +++ b/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs @@ -0,0 +1,43 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Windows.Media; +using Artemis.Core; +using Artemis.Core.Services; +using SkiaSharp; +using SkiaSharp.Views.WPF; + +namespace Artemis.UI.Shared +{ + /// + /// Provides UI-oriented utilities for types + /// + public static class TypeUtilities + { + internal static INodeService? NodeService; + + /// + /// Creates a tuple containing a color and a slightly darkened color for a given type + /// + /// The type to create the color for + public static (Color, Color) GetTypeColors(Type type) + { + if (type == typeof(object)) + return (new SKColor(0xFFFFFF).ToColor(), new SKColor(0xFFFFFF).Darken(0.35f).ToColor()); + + TypeColorRegistration? typeColorRegistration = NodeService?.GetTypeColor(type); + if (typeColorRegistration != null) + return (typeColorRegistration.Color.ToColor(), typeColorRegistration.DarkenedColor.ToColor()); + + // Come up with a random color based on the type name that should be the same each time + MD5 md5Hasher = MD5.Create(); + byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(type.FullName!)); + int hash = BitConverter.ToInt32(hashed, 0); + + SKColor baseColor = SKColor.FromHsl(hash % 255, 50 + hash % 50, 50); + SKColor darkenedColor = baseColor.Darken(0.35f); + + return (baseColor.ToColor(), darkenedColor.ToColor()); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index a587da7cc..08f3659e9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -94,7 +94,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void Update(RenderProfileElement renderProfileElement) { - if (RenderProfileElement != null) RenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; + if (RenderProfileElement != null) + RenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; RenderProfileElement = renderProfileElement; @@ -102,7 +103,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions NotifyOfPropertyChange(nameof(AlwaysFinishTimeline)); NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled)); - RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; + if (RenderProfileElement != null) + RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; } #region Event handlers diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index ec5633647..5adbd456b 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Linq; using Artemis.Core; using Artemis.Core.Services; @@ -12,6 +13,7 @@ using Artemis.UI.Shared.Services; using Artemis.UI.SkiaSharp; using Artemis.VisualScripting.Nodes; using Serilog; +using SkiaSharp; namespace Artemis.UI.Services { @@ -119,6 +121,13 @@ namespace Artemis.UI.Services public void RegisterBuiltInNodeTypes() { + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(bool), new SKColor(0xFFCD3232)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(string), new SKColor(0xFFFFD700)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(int), new SKColor(0xFF32CD32)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(double), new SKColor(0xFF1E90FF)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(float), new SKColor(0xFFFF7C00)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFC842FF)); + foreach (Type nodeType in typeof(SumIntegersNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface)) _nodeService.RegisterNodeType(Constants.CorePlugin, nodeType); } diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs index 85f6a8a7c..22bfdcf07 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptCablePresenter.cs @@ -7,6 +7,7 @@ using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using Artemis.Core; +using Artemis.UI.Shared; using Artemis.VisualScripting.Editor.Controls.Wrapper; namespace Artemis.VisualScripting.Editor.Controls @@ -51,7 +52,7 @@ namespace Artemis.VisualScripting.Editor.Controls public Point ValuePosition { - get => (Point)GetValue(ValuePositionProperty); + get => (Point) GetValue(ValuePositionProperty); set => SetValue(ValuePositionProperty, value); } @@ -63,7 +64,7 @@ namespace Artemis.VisualScripting.Editor.Controls { _path = GetTemplateChild(PART_PATH) as Path ?? throw new NullReferenceException($"The Path '{PART_PATH}' is missing."); _path.MouseDown += OnPathMouseDown; - + Unloaded += OnUnloaded; } @@ -110,7 +111,7 @@ namespace Artemis.VisualScripting.Editor.Controls UpdateBorderBrush(); UpdateValuePosition(); } - + private void OnPinPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(VisualScriptPin.AbsolutePosition)) @@ -119,15 +120,15 @@ namespace Artemis.VisualScripting.Editor.Controls private void UpdateBorderBrush() { - // if (Cable.From.Pin.Type.IsAssignableTo(typeof(IList))) - // BorderBrush = new SolidColorBrush(Colors.MediumPurple); + (Color color, Color _) = TypeUtilities.GetTypeColors(Cable.From.Pin.Type); + BorderBrush = new SolidColorBrush(color); } private void UpdateValuePosition() { if (Cable.From == null || Cable.To == null) return; - + ValuePosition = new Point( Cable.From.AbsolutePosition.X + ((Cable.To.AbsolutePosition.X - Cable.From.AbsolutePosition.X) / 2), Cable.From.AbsolutePosition.Y + ((Cable.To.AbsolutePosition.Y - Cable.From.AbsolutePosition.Y) / 2) diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs index 76c15b45c..88154da03 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs @@ -5,6 +5,7 @@ using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using Artemis.Core; +using Artemis.UI.Shared; using Artemis.VisualScripting.Editor.Controls.Wrapper; namespace Artemis.VisualScripting.Editor.Controls @@ -108,9 +109,17 @@ namespace Artemis.VisualScripting.Editor.Controls if (args.NewValue is VisualScriptPin newPin) newPin.Node.Node.PropertyChanged += OnNodePropertyChanged; + UpdateBrushes(); UpdateAbsoluteLocation(); } + private void UpdateBrushes() + { + (Color border, Color background) = TypeUtilities.GetTypeColors(Pin.Pin.Type); + Background = new SolidColorBrush(background); + BorderBrush = new SolidColorBrush(border); + } + private void UpdateAbsoluteLocation() { if ((Pin == null) || (_nodePresenter == null)) return; diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml index 0a507021d..223a2b870 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml @@ -62,26 +62,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPinPresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPinPresenter.xaml index 2a6deed99..f485928e1 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPinPresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPinPresenter.xaml @@ -80,31 +80,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - From 05d82229910311be1b7283cb7aed1f2f3a6c773d Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 19 Aug 2021 16:13:37 +0200 Subject: [PATCH 013/270] Nodes - Ensure models using scripts always have a script --- .../Profile/DataBindings/DataBinding.cs | 19 ++++--- .../Models/Profile/RenderProfileElement.cs | 21 +++++--- .../ProfileConfiguration.cs | 15 ++++-- .../VisualScripting/Interfaces/INodeScript.cs | 1 + .../VisualScripting/NodeScript.cs | 53 +++++++------------ .../DisplayConditionsView.xaml | 18 +++++-- .../StaticIntegerValueNodeCustomViewModel.cs | 7 ++- 7 files changed, 78 insertions(+), 56 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 62fcaea73..c95480bae 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -17,9 +17,10 @@ namespace Artemis.Core { LayerProperty = dataBindingRegistration.LayerProperty; Entity = new DataBindingEntity(); - Script = new NodeScript(LayerProperty.PropertyDescription.Name ?? LayerProperty.Path, ""); ApplyRegistration(dataBindingRegistration); + Script = new NodeScript(GetScriptName(), "The value to put into the data binding"); + Save(); } @@ -27,7 +28,7 @@ namespace Artemis.Core { LayerProperty = layerProperty; Entity = entity; - Script = new NodeScript(LayerProperty.PropertyDescription.Name ?? LayerProperty.Path, ""); + Script = new NodeScript(GetScriptName(), "The value to put into the data binding"); // Load will add children so be initialized before that Load(); @@ -116,8 +117,7 @@ namespace Artemis.Core if (Registration != null) Registration.DataBinding = null; - Script?.Dispose(); - Script = null; + Script.Dispose(); } } @@ -213,6 +213,13 @@ namespace Artemis.Core _reapplyValue = true; } + private string GetScriptName() + { + if (LayerProperty.GetAllDataBindingRegistrations().Count == 1) + return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path; + return $"{LayerProperty.PropertyDescription.Name ?? LayerProperty.Path} - {Registration?.DisplayName}"; + } + /// public void Dispose() { @@ -238,8 +245,8 @@ namespace Artemis.Core Script.Dispose(); Script = Entity.NodeScript != null - ? new NodeScript(Entity.NodeScript) - : new NodeScript(LayerProperty.PropertyDescription.Name ?? LayerProperty.Path, ""); + ? new NodeScript(GetScriptName(), "The value to put into the data binding", Entity.NodeScript) + : new NodeScript(GetScriptName(), "The value to put into the data binding"); } /// diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 8d69a0980..7e841350b 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -18,13 +18,17 @@ namespace Artemis.Core { private SKRectI _bounds; private SKPath? _path; + private readonly string _typeDisplayName; internal RenderProfileElement(Profile profile) : base(profile) { + _typeDisplayName = this is Layer ? "layer" : "folder"; + _displayCondition = new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active"); + Timeline = new Timeline(); ExpandedPropertyGroups = new List(); LayerEffectsList = new List(); - + LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; } @@ -59,15 +63,17 @@ namespace Artemis.Core foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.Dispose(); - DisplayCondition?.Dispose(); + DisplayCondition.Dispose(); base.Dispose(disposing); } internal void LoadRenderElement() { - DisplayCondition = RenderElementEntity.NodeScript != null ? new NodeScript(RenderElementEntity.NodeScript) : null; - + DisplayCondition = RenderElementEntity.NodeScript != null + ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript) + : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active"); + Timeline = RenderElementEntity.Timeline != null ? new Timeline(RenderElementEntity.Timeline) : new Timeline(); @@ -354,14 +360,15 @@ namespace Artemis.Core protected set => SetAndNotify(ref _displayConditionMet, value); } - private NodeScript? _displayCondition; + private NodeScript _displayCondition; private bool _displayConditionMet; private bool _toggledOnByEvent = false; + /// /// Gets or sets the root display condition group /// - public NodeScript? DisplayCondition + public NodeScript DisplayCondition { get => _displayCondition; set => SetAndNotify(ref _displayCondition, value); @@ -378,7 +385,7 @@ namespace Artemis.Core return; } - if (DisplayCondition == null) + if (!DisplayCondition.HasNodes) { DisplayConditionMet = true; return; diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index 162567ab7..2f29cdd2a 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -28,6 +28,7 @@ namespace Artemis.Core Entity = new ProfileConfigurationEntity(); Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon}; + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active"); } internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) @@ -38,6 +39,8 @@ namespace Artemis.Core Entity = entity; Icon = new ProfileConfigurationIcon(Entity); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active"); + Load(); } @@ -130,7 +133,7 @@ namespace Artemis.Core /// Gets the data model condition that must evaluate to for this profile to be activated /// alongside any activation requirements of the , if set /// - public NodeScript? ActivationCondition { get; set; } + public NodeScript ActivationCondition { get; set; } /// /// Gets or sets the module this profile uses @@ -168,7 +171,7 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("ProfileConfiguration"); - if (ActivationCondition == null) + if (!ActivationCondition.HasNodes) ActivationConditionMet = true; else { @@ -216,7 +219,7 @@ namespace Artemis.Core public void Dispose() { _disposed = true; - ActivationCondition?.Dispose(); + ActivationCondition.Dispose(); } #endregion @@ -237,8 +240,10 @@ namespace Artemis.Core Icon.Load(); - ActivationCondition?.Dispose(); - ActivationCondition = Entity.ActivationCondition != null ? new NodeScript(Entity.ActivationCondition) : null; + ActivationCondition.Dispose(); + ActivationCondition = Entity.ActivationCondition != null + ? new NodeScript("Activate profile", "Whether or not the profile should be active", Entity.ActivationCondition) + : new NodeScript("Activate profile", "Whether or not the profile should be active"); EnableHotkey = Entity.EnableHotkey != null ? new ProfileConfigurationHotkey(Entity.EnableHotkey) : null; DisableHotkey = Entity.DisableHotkey != null ? new ProfileConfigurationHotkey(Entity.DisableHotkey) : null; diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs index abfa87b75..c05ba214d 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs @@ -8,6 +8,7 @@ namespace Artemis.Core { string Name { get; } string Description { get; } + bool HasNodes { get; } IEnumerable Nodes { get; } diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 8d30b5fbd..ecb992895 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -16,13 +16,13 @@ namespace Artemis.Core public string Name { get; } public string Description { get; } + public bool HasNodes => _nodes.Count > 1; private readonly List _nodes = new(); public IEnumerable Nodes => new ReadOnlyCollection(_nodes); protected INode ExitNode { get; set; } public abstract Type ResultType { get; } - public abstract void CreateExitNode(string name, string description = ""); #endregion @@ -38,14 +38,12 @@ namespace Artemis.Core NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeChanged; } - internal NodeScript(NodeScriptEntity entity) + internal NodeScript(string name, string description, NodeScriptEntity entity) { - this.Name = entity.Name; - this.Description = entity.Description; + this.Name = name; + this.Description = description; this.Entity = entity; - Load(); - NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeChanged; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeChanged; } @@ -85,40 +83,25 @@ namespace Artemis.Core /// public void Load() { - bool gotExitNode = false; - // Create nodes Dictionary nodes = new(); foreach (NodeEntity entityNode in Entity.Nodes) { - INode? node = LoadNode(entityNode); + INode? node = LoadNode(entityNode, entityNode.IsExitNode ? ExitNode : null); if (node == null) continue; - - if (node.IsExitNode) - gotExitNode = true; - nodes.Add(entityNode.Id, node); } - - if (!gotExitNode) - CreateExitNode("Exit node"); - + LoadConnections(nodes); _nodes.Clear(); _nodes.AddRange(nodes.Values); } - private INode? LoadNode(NodeEntity nodeEntity) + private INode? LoadNode(NodeEntity nodeEntity, INode? node) { - INode node; - if (nodeEntity.IsExitNode) - { - CreateExitNode(nodeEntity.Name, nodeEntity.Description); - node = ExitNode; - } - else + if (node == null) { NodeTypeRegistration? nodeTypeRegistration = NodeTypeStore.Get(nodeEntity.PluginId, nodeEntity.Type); if (nodeTypeRegistration == null) @@ -127,7 +110,12 @@ namespace Artemis.Core // Create the node node = nodeTypeRegistration.NodeData.CreateNode(nodeEntity); } - + else + { + node.X = nodeEntity.X; + node.Y = nodeEntity.Y; + } + // Restore pin collections foreach (NodePinCollectionEntity entityNodePinCollection in nodeEntity.PinCollections) { @@ -280,18 +268,17 @@ namespace Artemis.Core public override Type ResultType => typeof(T); - public override void CreateExitNode(string name, string description = "") - { - ExitNode = new ExitNode(name, description); - } - #endregion #region Constructors - internal NodeScript(NodeScriptEntity entity) - : base(entity) + internal NodeScript(string name, string description, NodeScriptEntity entity) + : base(name, description, entity) { + ExitNode = new ExitNode(name, description); + AddNode(ExitNode); + + Load(); } public NodeScript(string name, string description) diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index a9f8b3867..16a6b7000 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -45,7 +45,10 @@ - + + + + - Click to edit script + + + + + + + Click to edit script + + + diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs index dc18ea51d..14e63812c 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs @@ -13,7 +13,12 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels public int Input { - get => (int) _node.Storage; + get + { + if (_node.Storage is long longInput) + return (int) longInput; + return (int) _node.Storage; + } set { _node.Storage = value; From c6a73f01f01c40acb15a0bdc4bdf0cdfe28a57ed Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 19 Aug 2021 23:58:32 +0200 Subject: [PATCH 014/270] Node editor - Tweak grid size and apply grid size to script Type coloring - Fix object's white color --- src/Artemis.UI.Shared/Utilities/TypeUtilities.cs | 2 +- .../Editor/Controls/VisualScriptPresenter.cs | 2 +- .../Editor/Styles/VisualScriptPresenter.xaml | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs b/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs index b979006e4..314cdb989 100644 --- a/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs +++ b/src/Artemis.UI.Shared/Utilities/TypeUtilities.cs @@ -23,7 +23,7 @@ namespace Artemis.UI.Shared public static (Color, Color) GetTypeColors(Type type) { if (type == typeof(object)) - return (new SKColor(0xFFFFFF).ToColor(), new SKColor(0xFFFFFF).Darken(0.35f).ToColor()); + return (SKColors.White.ToColor(), SKColors.White.Darken(0.35f).ToColor()); TypeColorRegistration? typeColorRegistration = NodeService?.GetTypeColor(type); if (typeColorRegistration != null) diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index bd47ec9ee..d7c4016c9 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -121,7 +121,7 @@ namespace Artemis.VisualScripting.Editor.Controls } public static readonly DependencyProperty GridSizeProperty = DependencyProperty.Register( - "GridSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(24)); + "GridSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(12)); public int GridSize { diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml index afa908aa7..eb9341488 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptPresenter.xaml @@ -87,13 +87,13 @@ - + - - - - + + + + From 3ed3cd1b1ee17794f2b9c08c736fba2a8c35e95b Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 20 Aug 2021 00:14:11 +0200 Subject: [PATCH 015/270] UI - Fix double registration of default UI types --- src/Artemis.UI/Screens/RootViewModel.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index b5edea137..fc35027f8 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -32,11 +32,12 @@ namespace Artemis.UI.Screens private readonly IWindowManager _windowManager; private readonly PluginSetting _windowSize; + private static bool _registeredBuiltInTypes; private bool _lostFocus; private ISnackbarMessageQueue _mainMessageQueue; private MaterialWindow _window; private string _windowTitle; - + public RootViewModel( IKernel kernel, IEventAggregator eventAggregator, @@ -169,10 +170,14 @@ namespace Artemis.UI.Screens SidebarViewModel.SelectedScreenChanged += SidebarViewModelOnSelectedScreenChanged; ActiveItem = SidebarViewModel.SelectedScreen; - _builtInRegistrationService.RegisterBuiltInDataModelDisplays(); - _builtInRegistrationService.RegisterBuiltInDataModelInputs(); - _builtInRegistrationService.RegisterBuiltInPropertyEditors(); - _builtInRegistrationService.RegisterBuiltInNodeTypes(); + if (!_registeredBuiltInTypes) + { + _builtInRegistrationService.RegisterBuiltInDataModelDisplays(); + _builtInRegistrationService.RegisterBuiltInDataModelInputs(); + _builtInRegistrationService.RegisterBuiltInPropertyEditors(); + _builtInRegistrationService.RegisterBuiltInNodeTypes(); + _registeredBuiltInTypes = true; + } _window = (MaterialWindow) View; From 836e979991e9b56a64f4e35d5f620a701023aa30 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 21 Aug 2021 12:15:01 +0200 Subject: [PATCH 016/270] Nodes - Provide scripts with a context Nodes - Inform nodes about the script they're being initialized for Nodes - Added float nodes matching the existing other number types Core - Add API for retrieving data binding values via the interface --- .../Profile/DataBindings/DataBinding.cs | 8 +- .../DataBindings/DataBindingRegistration.cs | 9 ++ .../DataBindings/IDataBindingRegistration.cs | 15 +++- .../Models/Profile/RenderProfileElement.cs | 6 +- .../ProfileConfiguration.cs | 8 +- src/Artemis.Core/Services/NodeService.cs | 6 +- .../VisualScripting/CustomNodeViewModel.cs | 8 +- .../VisualScripting/Interfaces/INode.cs | 2 +- .../VisualScripting/Interfaces/INodeScript.cs | 2 + src/Artemis.Core/VisualScripting/Node.cs | 9 +- src/Artemis.Core/VisualScripting/NodeData.cs | 6 +- .../VisualScripting/NodeScript.cs | 18 ++-- .../DisplayConditionsViewModel.cs | 2 - .../Artemis.VisualScripting.csproj | 3 + .../Editor/Controls/VisualScriptPresenter.cs | 3 +- .../CustomViewModels/CustomNodeViewModel.cs | 34 +++++++ .../DataModelNodeCustomViewModel.cs | 32 +++++++ .../StaticDoubleValueNodeCustomViewModel.cs | 24 ----- .../StaticIntegerValueNodeCustomViewModel.cs | 29 ------ .../StaticStringValueNodeCustomViewModel.cs | 24 ----- .../StaticValueNodeViewModels.cs | 89 +++++++++++++++++++ .../CustomViews/DataModelNodeCustomView.xaml | 2 +- .../StaticFloatValueNodeCustomView.xaml | 12 +++ .../Nodes/DataModelNode.cs | 9 +- .../Nodes/StaticValueNodes.cs | 35 +++++++- src/Artemis.VisualScripting/Nodes/SumNode.cs | 32 +++++++ 26 files changed, 305 insertions(+), 122 deletions(-) create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs delete mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs delete mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs delete mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViews/StaticFloatValueNodeCustomView.xaml diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index c95480bae..0cf5fc9fb 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -19,7 +19,7 @@ namespace Artemis.Core Entity = new DataBindingEntity(); ApplyRegistration(dataBindingRegistration); - Script = new NodeScript(GetScriptName(), "The value to put into the data binding"); + Script = new NodeScript(GetScriptName(), "The value to put into the data binding", LayerProperty.ProfileElement.Profile); Save(); } @@ -28,7 +28,7 @@ namespace Artemis.Core { LayerProperty = layerProperty; Entity = entity; - Script = new NodeScript(GetScriptName(), "The value to put into the data binding"); + Script = new NodeScript(GetScriptName(), "The value to put into the data binding", LayerProperty.ProfileElement.Profile); // Load will add children so be initialized before that Load(); @@ -245,8 +245,8 @@ namespace Artemis.Core Script.Dispose(); Script = Entity.NodeScript != null - ? new NodeScript(GetScriptName(), "The value to put into the data binding", Entity.NodeScript) - : new NodeScript(GetScriptName(), "The value to put into the data binding"); + ? new NodeScript(GetScriptName(), "The value to put into the data binding", Entity.NodeScript, LayerProperty.ProfileElement.Profile) + : new NodeScript(GetScriptName(), "The value to put into the data binding", LayerProperty.ProfileElement.Profile); } /// diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs index ab50c44c9..cab335a43 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs @@ -40,6 +40,9 @@ namespace Artemis.Core /// public string DisplayName { get; } + /// + public Type ValueType => typeof(TProperty); + /// /// Gets the data binding created using this registration /// @@ -74,5 +77,11 @@ namespace Artemis.Core // The related entity is left behind, just in case the data binding is added back later LayerProperty.DisableDataBinding(DataBinding); } + + /// + public object? GetValue() + { + return Getter(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs index c1321d4c3..4b9ceeb94 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs @@ -1,4 +1,6 @@ -namespace Artemis.Core +using System; + +namespace Artemis.Core { /// /// Represents a data binding registration @@ -10,6 +12,11 @@ /// string DisplayName { get; } + /// + /// Gets the type of the value this data binding registration points to + /// + Type ValueType { get; } + /// /// Returns the data binding applied using this registration /// @@ -25,5 +32,11 @@ /// If present, removes the current data binding /// void ClearDataBinding(); + + /// + /// Gets the value of the data binding + /// + /// A value matching the type of + object? GetValue(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 7e841350b..7003dd985 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -23,7 +23,7 @@ namespace Artemis.Core internal RenderProfileElement(Profile profile) : base(profile) { _typeDisplayName = this is Layer ? "layer" : "folder"; - _displayCondition = new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active"); + _displayCondition = new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); Timeline = new Timeline(); ExpandedPropertyGroups = new List(); @@ -71,8 +71,8 @@ namespace Artemis.Core internal void LoadRenderElement() { DisplayCondition = RenderElementEntity.NodeScript != null - ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript) - : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active"); + ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript, Profile) + : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); Timeline = RenderElementEntity.Timeline != null ? new Timeline(RenderElementEntity.Timeline) diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index 2f29cdd2a..b2ce08edb 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -28,7 +28,7 @@ namespace Artemis.Core Entity = new ProfileConfigurationEntity(); Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon}; - ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active"); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); } internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) @@ -39,7 +39,7 @@ namespace Artemis.Core Entity = entity; Icon = new ProfileConfigurationIcon(Entity); - ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active"); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); Load(); } @@ -242,8 +242,8 @@ namespace Artemis.Core ActivationCondition.Dispose(); ActivationCondition = Entity.ActivationCondition != null - ? new NodeScript("Activate profile", "Whether or not the profile should be active", Entity.ActivationCondition) - : new NodeScript("Activate profile", "Whether or not the profile should be active"); + ? new NodeScript("Activate profile", "Whether or not the profile should be active", Entity.ActivationCondition, this) + : new NodeScript("Activate profile", "Whether or not the profile should be active", this); EnableHotkey = Entity.EnableHotkey != null ? new ProfileConfigurationHotkey(Entity.EnableHotkey) : null; DisableHotkey = Entity.DisableHotkey != null ? new ProfileConfigurationHotkey(Entity.DisableHotkey) : null; diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 66ffd7804..fd09d8de5 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -53,7 +53,7 @@ namespace Artemis.Core.Services string description = nodeAttribute?.Description ?? string.Empty; string category = nodeAttribute?.Category ?? string.Empty; - NodeData nodeData = new(plugin, nodeType, name, description, category, e => CreateNode(e, nodeType)); + NodeData nodeData = new(plugin, nodeType, name, description, category, (s, e) => CreateNode(s, e, nodeType)); return NodeTypeStore.Add(nodeData); } @@ -65,7 +65,7 @@ namespace Artemis.Core.Services return NodeTypeStore.AddColor(type, color, plugin); } - private INode CreateNode(NodeEntity? entity, Type nodeType) + private INode CreateNode(INodeScript script, NodeEntity? entity, Type nodeType) { INode node = _kernel.Get(nodeType) as INode ?? throw new InvalidOperationException($"Node {nodeType} is not an INode"); @@ -79,7 +79,7 @@ namespace Artemis.Core.Services if (node is CustomViewModelNode customViewModelNode) customViewModelNode.BaseCustomViewModel = _kernel.Get(customViewModelNode.CustomViewModelType, new ConstructorArgument("node", node)); - node.Initialize(); + node.Initialize(script); return node; } diff --git a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs index c53cf951e..5975ed806 100644 --- a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs +++ b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs @@ -2,14 +2,8 @@ namespace Artemis.Core { - public class CustomNodeViewModel : CorePropertyChanged + public interface ICustomNodeViewModel { - [JsonIgnore] public INode Node { get; } - - public CustomNodeViewModel(INode node) - { - Node = node; - } } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs index 5a548a4b9..f17fd3352 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs @@ -19,7 +19,7 @@ namespace Artemis.Core event EventHandler Resetting; - void Initialize(); + void Initialize(INodeScript script); void Evaluate(); void Reset(); } diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs index c05ba214d..76a6df648 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs @@ -14,6 +14,8 @@ namespace Artemis.Core Type ResultType { get; } + object? Context { get; set; } + void Run(); void AddNode(INode node); void RemoveNode(INode node); diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 90a5f404e..5b504a8ec 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; namespace Artemis.Core { @@ -107,7 +108,7 @@ namespace Artemis.Core OnPropertyChanged(nameof(Pins)); return pin; } - + protected bool RemovePin(Pin pin) { bool isRemoved = _pins.Remove(pin); @@ -136,7 +137,9 @@ namespace Artemis.Core return pin; } - public virtual void Initialize() { } + public virtual void Initialize(INodeScript script) + { + } public abstract void Evaluate(); @@ -148,7 +151,7 @@ namespace Artemis.Core #endregion } - public abstract class Node : CustomViewModelNode where T : CustomNodeViewModel + public abstract class Node : CustomViewModelNode where T : ICustomNodeViewModel { protected Node() { diff --git a/src/Artemis.Core/VisualScripting/NodeData.cs b/src/Artemis.Core/VisualScripting/NodeData.cs index 7d21a0438..8a13d4580 100644 --- a/src/Artemis.Core/VisualScripting/NodeData.cs +++ b/src/Artemis.Core/VisualScripting/NodeData.cs @@ -14,13 +14,13 @@ namespace Artemis.Core public string Description { get; } public string Category { get; } - private Func _create; + private Func _create; #endregion #region Constructors - internal NodeData(Plugin plugin, Type type, string name, string description, string category, Func? create) + internal NodeData(Plugin plugin, Type type, string name, string description, string category, Func? create) { this.Plugin = plugin; this.Type = type; @@ -34,7 +34,7 @@ namespace Artemis.Core #region Methods - public INode CreateNode(NodeEntity? entity) => _create(entity); + public INode CreateNode(INodeScript script, NodeEntity? entity) => _create(script, entity); #endregion } diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index ecb992895..8f675a56e 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -24,25 +24,29 @@ namespace Artemis.Core protected INode ExitNode { get; set; } public abstract Type ResultType { get; } + public object? Context { get; set; } + #endregion #region Constructors - public NodeScript(string name, string description) + public NodeScript(string name, string description, object? context = null) { this.Name = name; this.Description = description; + this.Context = context; this.Entity = new NodeScriptEntity(); NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeChanged; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeChanged; } - internal NodeScript(string name, string description, NodeScriptEntity entity) + internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null) { this.Name = name; this.Description = description; this.Entity = entity; + this.Context = context; NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeChanged; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeChanged; @@ -108,7 +112,7 @@ namespace Artemis.Core return null; // Create the node - node = nodeTypeRegistration.NodeData.CreateNode(nodeEntity); + node = nodeTypeRegistration.NodeData.CreateNode(this, nodeEntity); } else { @@ -272,8 +276,8 @@ namespace Artemis.Core #region Constructors - internal NodeScript(string name, string description, NodeScriptEntity entity) - : base(name, description, entity) + internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null) + : base(name, description, entity, context) { ExitNode = new ExitNode(name, description); AddNode(ExitNode); @@ -281,8 +285,8 @@ namespace Artemis.Core Load(); } - public NodeScript(string name, string description) - : base(name, description) + public NodeScript(string name, string description, object? context = null) + : base(name, description, context) { ExitNode = new ExitNode(name, description); AddNode(ExitNode); diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 08f3659e9..72f4bfb91 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -116,8 +116,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (RenderProfileElement == null) return; - RenderProfileElement.DisplayCondition ??= new NodeScript("End Result", ""); - _windowManager.ShowDialog(_nodeVmFactory.NodeScriptWindowViewModel(RenderProfileElement.DisplayCondition)); _profileEditorService.SaveSelectedProfileElement(); } diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index 93f3b3450..85b4c6b8e 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -50,6 +50,9 @@ $(DefaultXamlRuntime) Designer + + $(DefaultXamlRuntime) + $(DefaultXamlRuntime) diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index d7c4016c9..bc92fa645 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -383,7 +383,8 @@ namespace Artemis.VisualScripting.Editor.Controls if (_creationBoxParent.ContextMenu != null) _creationBoxParent.ContextMenu.IsOpen = false; - INode node = nodeData.CreateNode(null); + INode node = nodeData.CreateNode(Script, null); + node.Initialize(Script); Script.AddNode(node); InitializeNode(node, _lastRightClickLocation); diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs new file mode 100644 index 000000000..fca28dbbb --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs @@ -0,0 +1,34 @@ +using System.Windows; +using Artemis.Core; +using Stylet; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels +{ + public abstract class CustomNodeViewModel : PropertyChangedBase, IViewAware, ICustomNodeViewModel + { + protected CustomNodeViewModel(INode node) + { + Node = node; + } + + public INode Node { get; } + + #region Implementation of IViewAware + + /// + public void AttachView(UIElement view) + { + View = view; + OnDisplay(); + } + + protected virtual void OnDisplay() + { + } + + /// + public UIElement View { get; private set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs index 4e18dd0b1..126a1815d 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs @@ -1,16 +1,25 @@ using Artemis.Core; +using Artemis.Core.Modules; +using Stylet; namespace Artemis.VisualScripting.Nodes.CustomViewModels { public class DataModelNodeCustomViewModel : CustomNodeViewModel { private readonly DataModelNode _node; + private BindableCollection _modules; public DataModelNodeCustomViewModel(DataModelNode node) : base(node) { _node = node; } + public BindableCollection Modules + { + get => _modules; + set => SetAndNotify(ref _modules, value); + } + public DataModelPath DataModelPath { get => _node.DataModelPath; @@ -32,5 +41,28 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels _node.UpdateOutputPin(); } } + + public void Initialize() + { + if (Modules != null) + return; + + Modules = new BindableCollection(); + if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + Modules.Add(scriptProfile.Configuration.Module); + else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + Modules.Add(profileConfiguration.Module); + } + + #region Overrides of CustomNodeViewModel + + /// + protected override void OnDisplay() + { + Initialize(); + base.OnDisplay(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs deleted file mode 100644 index 1a0db0a2e..000000000 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticDoubleValueNodeCustomViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Artemis.Core; - -namespace Artemis.VisualScripting.Nodes.CustomViewModels -{ - public class StaticDoubleValueNodeCustomViewModel : CustomNodeViewModel - { - private readonly StaticDoubleValueNode _node; - - public StaticDoubleValueNodeCustomViewModel(StaticDoubleValueNode node) : base(node) - { - _node = node; - } - - public double Input - { - get => (double) _node.Storage; - set - { - _node.Storage = value; - OnPropertyChanged(nameof(Input)); - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs deleted file mode 100644 index 14e63812c..000000000 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticIntegerValueNodeCustomViewModel.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Artemis.Core; - -namespace Artemis.VisualScripting.Nodes.CustomViewModels -{ - public class StaticIntegerValueNodeCustomViewModel : CustomNodeViewModel - { - private readonly StaticIntegerValueNode _node; - - public StaticIntegerValueNodeCustomViewModel(StaticIntegerValueNode node) : base(node) - { - _node = node; - } - - public int Input - { - get - { - if (_node.Storage is long longInput) - return (int) longInput; - return (int) _node.Storage; - } - set - { - _node.Storage = value; - OnPropertyChanged(nameof(Input)); - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs deleted file mode 100644 index 1b6f3eae4..000000000 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticStringValueNodeCustomViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Artemis.Core; - -namespace Artemis.VisualScripting.Nodes.CustomViewModels -{ - public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel - { - private readonly StaticStringValueNode _node; - - public StaticStringValueNodeCustomViewModel(StaticStringValueNode node) : base(node) - { - _node = node; - } - - public string Input - { - get => (string) _node.Storage; - set - { - _node.Storage = value; - OnPropertyChanged(nameof(Input)); - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs new file mode 100644 index 000000000..17b09f66e --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs @@ -0,0 +1,89 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels +{ + public class StaticDoubleValueNodeCustomViewModel : CustomNodeViewModel + { + private readonly StaticDoubleValueNode _node; + + public StaticDoubleValueNodeCustomViewModel(StaticDoubleValueNode node) : base(node) + { + _node = node; + } + + public double Input + { + get => (double) _node.Storage; + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } + } + } + + public class StaticFloatValueNodeCustomViewModel : CustomNodeViewModel + { + private readonly StaticFloatValueNode _node; + + public StaticFloatValueNodeCustomViewModel(StaticFloatValueNode node) : base(node) + { + _node = node; + } + + public float Input + { + get => (float)_node.Storage; + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } + } + } + + public class StaticIntegerValueNodeCustomViewModel : CustomNodeViewModel + { + private readonly StaticIntegerValueNode _node; + + public StaticIntegerValueNodeCustomViewModel(StaticIntegerValueNode node) : base(node) + { + _node = node; + } + + public int Input + { + get + { + if (_node.Storage is long longInput) + return (int)longInput; + return (int)_node.Storage; + } + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } + } + } + + public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel + { + private readonly StaticStringValueNode _node; + + public StaticStringValueNodeCustomViewModel(StaticStringValueNode node) : base(node) + { + _node = node; + } + + public string Input + { + get => (string)_node.Storage; + set + { + _node.Storage = value; + OnPropertyChanged(nameof(Input)); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml index 4ced4397a..e1a1c9d39 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml +++ b/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml @@ -8,5 +8,5 @@ xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> - + diff --git a/src/Artemis.VisualScripting/Nodes/CustomViews/StaticFloatValueNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/CustomViews/StaticFloatValueNodeCustomView.xaml new file mode 100644 index 000000000..7cd2b5ad7 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViews/StaticFloatValueNodeCustomView.xaml @@ -0,0 +1,12 @@ + + + diff --git a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs index 53b172eb1..64b28a169 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs +++ b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs @@ -13,18 +13,21 @@ namespace Artemis.VisualScripting.Nodes { } + public INodeScript Script { get; private set; } public OutputPin Output { get; private set; } public DataModelPath DataModelPath { get; set; } - public override void Initialize() + public override void Initialize(INodeScript script) { - if (Storage is not DataModelPathEntity pathEntity) + Script = script; + + if (Storage is not DataModelPathEntity pathEntity) return; DataModelPath = new DataModelPath(null, pathEntity); UpdateOutputPin(); } - + public override void Evaluate() { if (DataModelPath.IsValid && Output != null) diff --git a/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs b/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs index eadf03c79..4d37fb320 100644 --- a/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs +++ b/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs @@ -29,7 +29,7 @@ namespace Artemis.VisualScripting.Nodes Output.Value = Storage as int? ?? 0; } - public override void Initialize() => Storage ??= 0; + public override void Initialize(INodeScript script) => Storage ??= 0; #endregion } @@ -60,7 +60,38 @@ namespace Artemis.VisualScripting.Nodes Output.Value = Storage as double? ?? 0.0; } - public override void Initialize() => Storage ??= 0.0; + public override void Initialize(INodeScript script) => Storage ??= 0.0; + + #endregion + } + + [Node("Float-Value", "Outputs a configurable float value.")] + public class StaticFloatValueNode : Node + { + #region Properties & Fields + + public OutputPin Output { get; } + + #endregion + + #region Constructors + + public StaticFloatValueNode() + : base("Float", "Outputs a configurable float value.") + { + Output = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = Storage as float? ?? 0.0f; + } + + public override void Initialize(INodeScript script) => Storage ??= 0.0f; #endregion } diff --git a/src/Artemis.VisualScripting/Nodes/SumNode.cs b/src/Artemis.VisualScripting/Nodes/SumNode.cs index 555f6a7f1..27cf67955 100644 --- a/src/Artemis.VisualScripting/Nodes/SumNode.cs +++ b/src/Artemis.VisualScripting/Nodes/SumNode.cs @@ -35,6 +35,38 @@ namespace Artemis.VisualScripting.Nodes #endregion } + [Node("Sum (Float)", "Sums the connected float values.")] + public class SumFloatsNode : Node + { + #region Properties & Fields + + public InputPinCollection Values { get; } + + public OutputPin Sum { get; } + + #endregion + + #region Constructors + + public SumFloatsNode() + : base("Sum", "Sums the connected float values.") + { + Values = CreateInputPinCollection("Values", 2); + Sum = CreateOutputPin("Sum"); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Sum.Value = Values.Values.Sum(); + } + + #endregion + } + [Node("Sum (Double)", "Sums the connected double values.")] public class SumDoublesNode : Node { From a4667fdc037d8ab3d238eb376ef1248d2270ab13 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 21 Aug 2021 13:51:25 +0200 Subject: [PATCH 017/270] Profiles - Only load nodes after the entire profile structure loaded Profiles - Added API to get all render elements Nodes - Added layer property node --- .../Profile/DataBindings/DataBinding.cs | 4 + .../DataBindings/DataBindingRegistration.cs | 2 +- .../Profile/DataBindings/IDataBinding.cs | 5 + src/Artemis.Core/Models/Profile/Profile.cs | 17 ++- .../Models/Profile/ProfileElement.cs | 79 +++++++---- .../Models/Profile/RenderProfileElement.cs | 17 ++- .../Services/Storage/ProfileService.cs | 14 +- .../Visualization/ProfileViewModel.cs | 8 +- .../LayerPropertyNodeCustomViewModel.cs | 80 +++++++++++ .../LayerPropertyNodeCustomView.xaml | 18 +++ .../Nodes/LayerPropertyNode.cs | 130 ++++++++++++++++++ 11 files changed, 317 insertions(+), 57 deletions(-) create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs create mode 100644 src/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml create mode 100644 src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 0cf5fc9fb..e2392c79e 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -242,7 +242,11 @@ namespace Artemis.Core EasingTime = Entity.EasingTime; EasingFunction = (Easings.Functions) Entity.EasingFunction; + } + /// + public void LoadNodeScript() + { Script.Dispose(); Script = Entity.NodeScript != null ? new NodeScript(GetScriptName(), "The value to put into the data binding", Entity.NodeScript, LayerProperty.ProfileElement.Profile) diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs index cab335a43..e7d5ca1d1 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs @@ -67,7 +67,7 @@ namespace Artemis.Core DataBinding = new DataBinding(LayerProperty, dataBinding); return DataBinding; } - + /// public void ClearDataBinding() { diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs index 1f54ca146..3c7c995bd 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs @@ -19,5 +19,10 @@ namespace Artemis.Core /// Applies the data binding to the layer property /// void Apply(); + + /// + /// If the data binding is enabled, loads the node script for that data binding + /// + void LoadNodeScript(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index c0c3a2528..a9090e12d 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -206,12 +206,10 @@ namespace Artemis.Core } } + List renderElements = GetAllRenderElements(); + if (ProfileEntity.LastSelectedProfileElement != Guid.Empty) - { - LastSelectedProfileElement = GetAllFolders().FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); - if (LastSelectedProfileElement == null) - LastSelectedProfileElement = GetAllLayers().FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); - } + LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); else LastSelectedProfileElement = null; @@ -219,6 +217,10 @@ namespace Artemis.Core scriptConfiguration.Script?.Dispose(); ScriptConfigurations.Clear(); ScriptConfigurations.AddRange(ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))); + + // Load node scripts last since they may rely on the profile structure being in place + foreach (RenderProfileElement renderProfileElement in renderElements) + renderProfileElement.LoadNodeScript(); } internal override void Save() @@ -253,10 +255,7 @@ namespace Artemis.Core /// public override IEnumerable GetBrokenHierarchy() { - foreach (IBreakableModel breakableModel in GetAllFolders().SelectMany(folders => folders.GetBrokenHierarchy())) - yield return breakableModel; - foreach (IBreakableModel breakableModel in GetAllLayers().SelectMany(layer => layer.GetBrokenHierarchy())) - yield return breakableModel; + return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy()); } #endregion diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 81537b0ba..a36dea794 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -99,6 +99,13 @@ namespace Artemis.Core /// public bool Disposed { get; protected set; } + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Name ?? GetType().Name; + + #endregion + /// /// Updates the element /// @@ -121,13 +128,6 @@ namespace Artemis.Core return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; } - #region Overrides of BreakableModel - - /// - public override string BrokenDisplayName => Name ?? GetType().Name; - - #endregion - #region Hierarchy /// @@ -147,7 +147,9 @@ namespace Artemis.Core // Add to the end of the list if (order == null) + { ChildrenList.Add(child); + } // Insert at the given index else { @@ -191,6 +193,27 @@ namespace Artemis.Core ChildrenList[index].Order = index; } + /// + /// Returns a flattened list of all child render elements + /// + /// + public List GetAllRenderElements() + { + if (Disposed) + throw new ObjectDisposedException(GetType().Name); + + List elements = new(); + foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast()) + { + // Add all folders in this element + elements.Add(childElement); + // Add all folders in folders inside this element + elements.AddRange(childElement.GetAllRenderElements()); + } + + return elements; + } + /// /// Returns a flattened list of all child folders /// @@ -240,27 +263,6 @@ namespace Artemis.Core internal abstract void Load(); internal abstract void Save(); - #endregion - - #region IDisposable - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the profile element - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - } - } - #endregion #region Events @@ -292,5 +294,26 @@ namespace Artemis.Core } #endregion + + #region IDisposable + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the profile element + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 7003dd985..f6a87f5b0 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -70,10 +70,6 @@ namespace Artemis.Core internal void LoadRenderElement() { - DisplayCondition = RenderElementEntity.NodeScript != null - ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript, Profile) - : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); - Timeline = RenderElementEntity.Timeline != null ? new Timeline(RenderElementEntity.Timeline) : new Timeline(); @@ -111,6 +107,19 @@ namespace Artemis.Core Timeline?.Save(); } + internal void LoadNodeScript() + { + DisplayCondition = RenderElementEntity.NodeScript != null + ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript, Profile) + : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); + + foreach (ILayerProperty layerProperty in GetAllLayerProperties()) + { + foreach (IDataBindingRegistration dataBindingRegistration in layerProperty.GetAllDataBindingRegistrations()) + dataBindingRegistration.GetDataBinding()?.LoadNodeScript(); + } + } + internal void OnLayerEffectsUpdated() { LayerEffectsUpdated?.Invoke(this, EventArgs.Empty); diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 3f532faf3..b6221c19f 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -438,10 +438,8 @@ namespace Artemis.Core.Services profile.Save(); if (includeChildren) { - foreach (Folder folder in profile.GetAllFolders()) - folder.Save(); - foreach (Layer layer in profile.GetAllLayers()) - layer.Save(); + foreach (RenderProfileElement child in profile.GetAllRenderElements()) + child.Save(); } // If there are no changes, don't bother saving @@ -599,11 +597,9 @@ namespace Artemis.Core.Services profile.Save(); - foreach (Folder folder in profile.GetAllFolders()) - folder.Save(); - foreach (Layer layer in profile.GetAllLayers()) - layer.Save(); - + foreach (RenderProfileElement renderProfileElement in profile.GetAllRenderElements()) + renderProfileElement.Save(); + _logger.Debug("Adapt profile - Saving " + profile); profile.RedoStack.Clear(); profile.UndoStack.Push(memento); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index 6f4fea3d0..b0549cdd7 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -313,15 +313,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization if (SuspendedEditing || !_settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true).Value || _profileEditorService.SelectedProfile == null) return; - foreach (IDataBindingRegistration dataBindingRegistration in _profileEditorService.SelectedProfile.GetAllFolders() + foreach (IDataBindingRegistration dataBindingRegistration in _profileEditorService.SelectedProfile.GetAllRenderElements() .SelectMany(f => f.GetAllLayerProperties(), (f, p) => p) .SelectMany(p => p.GetAllDataBindingRegistrations())) dataBindingRegistration.GetDataBinding()?.UpdateWithDelta(delta); - foreach (IDataBindingRegistration dataBindingRegistration in _profileEditorService.SelectedProfile.GetAllLayers() - .SelectMany(f => f.GetAllLayerProperties(), (f, p) => p) - .SelectMany(p => p.GetAllDataBindingRegistrations())) - dataBindingRegistration.GetDataBinding()?.UpdateWithDelta(delta); - + // TODO: Only update when there are data bindings if (!_profileEditorService.Playing) _profileEditorService.UpdateProfilePreview(); diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs new file mode 100644 index 000000000..edf84d7e1 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Core; +using Stylet; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels +{ + public class LayerPropertyNodeCustomViewModel : CustomNodeViewModel + { + private readonly LayerPropertyNode _node; + + private RenderProfileElement _selectedProfileElement; + private ILayerProperty _selectedLayerProperty; + + public LayerPropertyNodeCustomViewModel(LayerPropertyNode node) : base(node) + { + _node = node; + } + + public BindableCollection ProfileElements { get; } = new(); + + public RenderProfileElement SelectedProfileElement + { + get => _selectedProfileElement; + set + { + if (!SetAndNotify(ref _selectedProfileElement, value)) return; + _node.ChangeProfileElement(_selectedProfileElement); + GetLayerProperties(); + } + } + + public BindableCollection LayerProperties { get; } = new(); + + public ILayerProperty SelectedLayerProperty + { + get => _selectedLayerProperty; + set + { + if (!SetAndNotify(ref _selectedLayerProperty, value)) return; + _node.ChangeLayerProperty(_selectedLayerProperty); + } + } + + private void GetProfileElements() + { + ProfileElements.Clear(); + if (_node.Script.Context is not Profile profile) + return; + + List elements = new(profile.GetAllRenderElements()); + + ProfileElements.AddRange(elements.OrderBy(e => e.Order)); + _selectedProfileElement = _node.ProfileElement; + NotifyOfPropertyChange(nameof(SelectedProfileElement)); + } + + private void GetLayerProperties() + { + LayerProperties.Clear(); + if (_node.ProfileElement == null) + return; + + LayerProperties.AddRange(_node.ProfileElement.GetAllLayerProperties().Where(l => l.GetAllDataBindingRegistrations().Any())); + _selectedLayerProperty = _node.LayerProperty; + NotifyOfPropertyChange(nameof(SelectedLayerProperty)); + } + + #region Overrides of CustomNodeViewModel + + /// + protected override void OnDisplay() + { + GetProfileElements(); + GetLayerProperties(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml new file mode 100644 index 000000000..42be19cb8 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs b/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs new file mode 100644 index 000000000..64f75d022 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Core; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes +{ + [Node("Layer/Folder Property", "Outputs the property of a selected layer or folder")] + public class LayerPropertyNode : Node + { + private readonly object _layerPropertyLock = new(); + + public INodeScript Script { get; private set; } + public RenderProfileElement ProfileElement { get; private set; } + public ILayerProperty LayerProperty { get; private set; } + + public override void Evaluate() + { + lock (_layerPropertyLock) + { + // In this case remove the pins so no further evaluations occur + if (LayerProperty == null) + { + CreatePins(); + return; + } + + List list = LayerProperty.GetAllDataBindingRegistrations(); + int index = 0; + foreach (IPin pin in Pins) + { + OutputPin outputPin = (OutputPin) pin; + IDataBindingRegistration dataBindingRegistration = list[index]; + index++; + + // TODO: Is this really non-nullable? + outputPin.Value = dataBindingRegistration.GetValue(); + } + } + } + + public override void Initialize(INodeScript script) + { + Script = script; + + if (script.Context is Profile profile) + profile.ChildRemoved += ProfileOnChildRemoved; + + LoadLayerProperty(); + } + + public void LoadLayerProperty() + { + lock (_layerPropertyLock) + { + if (Script.Context is not Profile profile) + return; + if (Storage is not LayerPropertyNodeEntity entity) + return; + + RenderProfileElement element = profile.GetAllRenderElements().FirstOrDefault(l => l.EntityId == entity.ElementId); + + ProfileElement = element; + LayerProperty = element?.GetAllLayerProperties().FirstOrDefault(p => p.Path == entity.PropertyPath); + CreatePins(); + } + } + + public void ChangeProfileElement(RenderProfileElement profileElement) + { + lock (_layerPropertyLock) + { + ProfileElement = profileElement; + LayerProperty = null; + + Storage = new LayerPropertyNodeEntity + { + ElementId = ProfileElement?.EntityId ?? Guid.Empty, + PropertyPath = null + }; + + CreatePins(); + } + } + + public void ChangeLayerProperty(ILayerProperty layerProperty) + { + lock (_layerPropertyLock) + { + LayerProperty = layerProperty; + + Storage = new LayerPropertyNodeEntity + { + ElementId = ProfileElement?.EntityId ?? Guid.Empty, + PropertyPath = LayerProperty?.Path + }; + + CreatePins(); + } + } + + private void CreatePins() + { + while (Pins.Any()) + RemovePin((Pin) Pins.First()); + + if (LayerProperty == null) + return; + + foreach (IDataBindingRegistration dataBindingRegistration in LayerProperty.GetAllDataBindingRegistrations()) + CreateOutputPin(dataBindingRegistration.ValueType, dataBindingRegistration.DisplayName); + } + + private void ProfileOnChildRemoved(object? sender, EventArgs e) + { + if (Script.Context is not Profile profile) + return; + + if (!profile.GetAllRenderElements().Contains(ProfileElement)) + ChangeProfileElement(null); + } + } + + public class LayerPropertyNodeEntity + { + public Guid ElementId { get; set; } + public string PropertyPath { get; set; } + } +} \ No newline at end of file From 405d5b756cebf250cb8b8130c482913302a41f19 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 21 Aug 2021 15:33:55 +0200 Subject: [PATCH 018/270] Nodes - Serialize node storage ourselves to ignore exceptions --- src/Artemis.Core/Services/NodeService.cs | 10 +++++++++- src/Artemis.Core/VisualScripting/Interfaces/INode.cs | 2 +- src/Artemis.Core/VisualScripting/NodeScript.cs | 4 ++-- .../Entities/Profile/Nodes/NodeEntity.cs | 2 +- src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs | 3 +++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index fd09d8de5..82cf2d4f7 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using Artemis.Storage.Entities.Profile.Nodes; +using Newtonsoft.Json; using Ninject; using Ninject.Parameters; using SkiaSharp; @@ -73,7 +74,14 @@ namespace Artemis.Core.Services { node.X = entity.X; node.Y = entity.Y; - node.Storage = entity.Storage; + try + { + node.Storage = CoreJson.DeserializeObject(entity.Storage, true); + } + catch + { + // ignored + } } if (node is CustomViewModelNode customViewModelNode) diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs index f17fd3352..5f415d987 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs @@ -12,7 +12,7 @@ namespace Artemis.Core public double X { get; set; } public double Y { get; set; } - public object Storage { get; set; } + public object? Storage { get; set; } public IReadOnlyCollection Pins { get; } public IReadOnlyCollection PinCollections { get; } diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 8f675a56e..ba35ed922 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -96,7 +96,7 @@ namespace Artemis.Core continue; nodes.Add(entityNode.Id, node); } - + LoadConnections(nodes); _nodes.Clear(); @@ -179,7 +179,7 @@ namespace Artemis.Core Type = node.GetType().Name, X = node.X, Y = node.Y, - Storage = node.Storage, + Storage = CoreJson.SerializeObject(node.Storage, true), Name = node.Name, Description = node.Description, IsExitNode = node.IsExitNode diff --git a/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs b/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs index 6f33425f0..8a99bb8f0 100644 --- a/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Nodes/NodeEntity.cs @@ -19,7 +19,7 @@ namespace Artemis.Storage.Entities.Profile.Nodes public bool IsExitNode { get; set; } public double X { get; set; } public double Y { get; set; } - public object Storage { get; set; } + public string Storage { get; set; } public List PinCollections { get; set; } } diff --git a/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs b/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs index 4d37fb320..c144e1847 100644 --- a/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs +++ b/src/Artemis.VisualScripting/Nodes/StaticValueNodes.cs @@ -88,6 +88,9 @@ namespace Artemis.VisualScripting.Nodes public override void Evaluate() { + if (Storage is double doubleValue) + Storage = (float) doubleValue; + Output.Value = Storage as float? ?? 0.0f; } From 7422a9667d6240749474951d97f0f416c9c3bfd3 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Sat, 21 Aug 2021 21:48:01 +0200 Subject: [PATCH 019/270] VisualScripting: Fixed Preview; Added auto-fit mode --- .../VisualScripting/Interfaces/INodeScript.cs | 5 +- .../VisualScripting/Interfaces/IPin.cs | 3 + .../Interfaces/IPinCollection.cs | 3 + src/Artemis.Core/VisualScripting/Node.cs | 32 ++-- .../VisualScripting/NodeScript.cs | 11 ++ src/Artemis.Core/VisualScripting/Pin.cs | 16 ++ .../VisualScripting/PinCollection.cs | 21 ++- .../DisplayConditionsView.xaml | 5 +- .../Editor/Controls/VisualScriptEditor.cs | 8 - .../Editor/Controls/VisualScriptPresenter.cs | 163 +++++++++++++++--- .../Editor/Controls/Wrapper/VisualScript.cs | 84 ++++++++- .../Controls/Wrapper/VisualScriptNode.cs | 44 ++++- .../Controls/Wrapper/VisualScriptPin.cs | 30 +++- .../Wrapper/VisualScriptPinCollection.cs | 33 +++- 14 files changed, 372 insertions(+), 86 deletions(-) diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs index 76a6df648..dec3cab46 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INodeScript.cs @@ -15,7 +15,10 @@ namespace Artemis.Core Type ResultType { get; } object? Context { get; set; } - + + event EventHandler? NodeAdded; + event EventHandler? NodeRemoved; + void Run(); void AddNode(INode node); void RemoveNode(INode node); diff --git a/src/Artemis.Core/VisualScripting/Interfaces/IPin.cs b/src/Artemis.Core/VisualScripting/Interfaces/IPin.cs index aef3352cf..0ea3179e4 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/IPin.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/IPin.cs @@ -16,6 +16,9 @@ namespace Artemis.Core bool IsEvaluated { get; set; } + event EventHandler PinConnected; + event EventHandler PinDisconnected; + void ConnectTo(IPin pin); void DisconnectFrom(IPin pin); } diff --git a/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs b/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs index 410f269ba..266c31338 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs @@ -9,6 +9,9 @@ namespace Artemis.Core PinDirection Direction { get; } Type Type { get; } + event EventHandler PinAdded; + event EventHandler PinRemoved; + IPin AddPin(); bool Remove(IPin pin); } diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 5b504a8ec..ac6b4da8a 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -1,18 +1,14 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; namespace Artemis.Core { public abstract class Node : CorePropertyChanged, INode { - public event EventHandler Resetting; - #region Properties & Fields private string _name; - public string Name { get => _name; @@ -20,7 +16,6 @@ namespace Artemis.Core } private string _description; - public string Description { get => _description; @@ -28,7 +23,6 @@ namespace Artemis.Core } private double _x; - public double X { get => _x; @@ -36,7 +30,6 @@ namespace Artemis.Core } private double _y; - public double Y { get => _y; @@ -44,7 +37,6 @@ namespace Artemis.Core } private object? _storage; - public object? Storage { get => _storage; @@ -61,11 +53,16 @@ namespace Artemis.Core #endregion + #region Events + + public event EventHandler Resetting; + + #endregion + #region Construtors protected Node() - { - } + { } protected Node(string name, string description) { @@ -138,8 +135,7 @@ namespace Artemis.Core } public virtual void Initialize(INodeScript script) - { - } + { } public abstract void Evaluate(); @@ -154,12 +150,10 @@ namespace Artemis.Core public abstract class Node : CustomViewModelNode where T : ICustomNodeViewModel { protected Node() - { - } + { } protected Node(string name, string description) : base(name, description) - { - } + { } public override Type CustomViewModelType => typeof(T); public T CustomViewModel => (T) BaseCustomViewModel!; @@ -169,13 +163,11 @@ namespace Artemis.Core { /// protected CustomViewModelNode() - { - } + { } /// protected CustomViewModelNode(string name, string description) : base(name, description) - { - } + { } public abstract Type CustomViewModelType { get; } public object? BaseCustomViewModel { get; set; } diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index ba35ed922..0094d8daa 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -28,6 +28,13 @@ namespace Artemis.Core #endregion + #region Events + + public event EventHandler? NodeAdded; + public event EventHandler? NodeRemoved; + + #endregion + #region Constructors public NodeScript(string name, string description, object? context = null) @@ -67,11 +74,15 @@ namespace Artemis.Core public void AddNode(INode node) { _nodes.Add(node); + + NodeAdded?.Invoke(this, node); } public void RemoveNode(INode node) { _nodes.Remove(node); + + NodeRemoved?.Invoke(this, node); } public void Dispose() diff --git a/src/Artemis.Core/VisualScripting/Pin.cs b/src/Artemis.Core/VisualScripting/Pin.cs index c1192b53c..be8f3f63d 100644 --- a/src/Artemis.Core/VisualScripting/Pin.cs +++ b/src/Artemis.Core/VisualScripting/Pin.cs @@ -27,6 +27,13 @@ namespace Artemis.Core #endregion + #region Events + + public event EventHandler PinConnected; + public event EventHandler PinDisconnected; + + #endregion + #region Constructors protected Pin(INode node, string name = "") @@ -46,18 +53,27 @@ namespace Artemis.Core { _connectedTo.Add(pin); OnPropertyChanged(nameof(ConnectedTo)); + + PinConnected?.Invoke(this, pin); } public void DisconnectFrom(IPin pin) { _connectedTo.Remove(pin); OnPropertyChanged(nameof(ConnectedTo)); + + PinDisconnected?.Invoke(this, pin); } public void DisconnectAll() { + List connectedPins = new(_connectedTo); + _connectedTo.Clear(); OnPropertyChanged(nameof(ConnectedTo)); + + foreach (IPin pin in connectedPins) + PinDisconnected?.Invoke(this, pin); } private void OnNodeResetting(object sender, EventArgs e) diff --git a/src/Artemis.Core/VisualScripting/PinCollection.cs b/src/Artemis.Core/VisualScripting/PinCollection.cs index 469abf5dc..4bce68cc1 100644 --- a/src/Artemis.Core/VisualScripting/PinCollection.cs +++ b/src/Artemis.Core/VisualScripting/PinCollection.cs @@ -20,6 +20,13 @@ namespace Artemis.Core #endregion + #region Events + + public event EventHandler PinAdded; + public event EventHandler PinRemoved; + + #endregion + #region Constructors protected PinCollection(INode node, string name, int initialCount) @@ -35,14 +42,26 @@ namespace Artemis.Core #region Methods + public IPin AddPin() { IPin pin = CreatePin(); _pins.Add(pin); + + PinAdded?.Invoke(this, pin); + return pin; } - public bool Remove(IPin pin) => _pins.Remove(pin); + public bool Remove(IPin pin) + { + bool removed = _pins.Remove(pin); + + if (removed) + PinRemoved?.Invoke(this, pin); + + return removed; + } protected abstract IPin CreatePin(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index 16a6b7000..44bbe92a3 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -43,8 +43,9 @@ - + diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs index 82977bcf5..8af40eb0b 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptEditor.cs @@ -7,10 +7,6 @@ namespace Artemis.VisualScripting.Editor.Controls { public class VisualScriptEditor : Control { - #region Properties & Fields - - #endregion - #region Dependency Properties public static readonly DependencyProperty ScriptProperty = DependencyProperty.Register( @@ -32,9 +28,5 @@ namespace Artemis.VisualScripting.Editor.Controls } #endregion - - #region Methods - - #endregion } } diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index bc92fa645..0b939b2a3 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -6,6 +6,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; +using System.Windows.Threading; using Artemis.Core; using Artemis.VisualScripting.Editor.Controls.Wrapper; using Artemis.VisualScripting.ViewModel; @@ -32,11 +33,13 @@ namespace Artemis.VisualScripting.Editor.Controls #region Properties & Fields + private bool _fitPending = false; private Canvas _canvas; private ItemsControl _nodeList; private ItemsControl _cableList; private Border _selectionBorder; private TranslateTransform _canvasViewPortTransform; + private ScaleTransform _canvasViewPortScale; private Panel _creationBoxParent; private Vector _viewportCenter = new(0, 0); @@ -121,7 +124,7 @@ namespace Artemis.VisualScripting.Editor.Controls } public static readonly DependencyProperty GridSizeProperty = DependencyProperty.Register( - "GridSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(12)); + "GridSize", typeof(int), typeof(VisualScriptPresenter), new PropertyMetadata(24)); public int GridSize { @@ -138,6 +141,15 @@ namespace Artemis.VisualScripting.Editor.Controls set => SetValue(SurfaceSizeProperty, value); } + public static readonly DependencyProperty AutoFitScriptProperty = DependencyProperty.Register( + "AutoFitScript", typeof(bool), typeof(VisualScriptPresenter), new PropertyMetadata(false, AutoFitScriptChanged)); + + public bool AutoFitScript + { + get => (bool)GetValue(AutoFitScriptProperty); + set => SetValue(AutoFitScriptProperty, value); + } + #endregion #region Constructors @@ -163,6 +175,7 @@ namespace Artemis.VisualScripting.Editor.Controls _canvas.AllowDrop = true; + _canvas.LayoutTransform = _canvasViewPortScale = new ScaleTransform(MaxScale, MaxScale); _canvas.RenderTransform = _canvasViewPortTransform = new TranslateTransform(0, 0); _canvas.MouseLeftButtonDown += OnCanvasMouseLeftButtonDown; _canvas.MouseLeftButtonUp += OnCanvasMouseLeftButtonUp; @@ -177,11 +190,22 @@ namespace Artemis.VisualScripting.Editor.Controls _cableList.ItemsSource = VisualScript?.Cables; } + private static void AutoFitScriptChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) + { + if (d is not VisualScriptPresenter scriptPresenter) return; + + if ((args.NewValue as bool?) == true) + scriptPresenter.FitScript(); + } + private void OnSizeChanged(object sender, SizeChangedEventArgs args) { if (sender is not VisualScriptPresenter scriptPresenter) return; - scriptPresenter.UpdatePanning(); + if (AutoFitScript) + scriptPresenter.FitScript(); + else + scriptPresenter.UpdatePanning(); } private static void ScriptChanged(DependencyObject d, DependencyPropertyChangedEventArgs args) @@ -194,28 +218,33 @@ namespace Artemis.VisualScripting.Editor.Controls private void ScriptChanged(VisualScript newScript) { if (VisualScript != null) + { VisualScript.PropertyChanged -= OnVisualScriptPropertyChanged; + VisualScript.NodeMoved -= OnVisualScriptNodeMoved; + VisualScript.NodeCollectionChanged -= OnVisualScriptNodeCollectionChanged; + } VisualScript = newScript; if (VisualScript != null) { VisualScript.PropertyChanged += OnVisualScriptPropertyChanged; + VisualScript.NodeMoved += OnVisualScriptNodeMoved; + VisualScript.NodeCollectionChanged += OnVisualScriptNodeCollectionChanged; if (_nodeList != null) _nodeList.ItemsSource = VisualScript?.Nodes; if (_cableList != null) _cableList.ItemsSource = VisualScript?.Cables; - - VisualScript.Nodes.Clear(); - foreach (INode node in VisualScript.Script.Nodes) - InitializeNode(node); } VisualScript?.RecreateCables(); - CenterAt(new Vector(0, 0)); + if (AutoFitScript) + FitScript(); + else + CenterAt(new Vector(0, 0)); } private void OnVisualScriptPropertyChanged(object sender, PropertyChangedEventArgs args) @@ -225,6 +254,18 @@ namespace Artemis.VisualScripting.Editor.Controls _cableList.ItemsSource = VisualScript.Cables; } + private void OnVisualScriptNodeMoved(object sender, EventArgs args) + { + if (AutoFitScript) + FitScript(); + } + + private void OnVisualScriptNodeCollectionChanged(object? sender, EventArgs e) + { + if (AutoFitScript) + FitScript(); + } + private void OnCanvasPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs args) { _lastRightClickLocation = args.GetPosition(_canvas); @@ -261,6 +302,12 @@ namespace Artemis.VisualScripting.Editor.Controls private void OnCanvasMouseRightButtonDown(object sender, MouseButtonEventArgs args) { + if (AutoFitScript) + { + args.Handled = true; + return; + } + _dragCanvas = true; _dragCanvasStartLocation = args.GetPosition(this); _dragCanvasStartOffset = _viewportCenter; @@ -289,7 +336,7 @@ namespace Artemis.VisualScripting.Editor.Controls { if (args.RightButton == MouseButtonState.Pressed) { - Vector newLocation = _dragCanvasStartOffset + (((args.GetPosition(this) - _dragCanvasStartLocation)) * (1.0 / Scale)); + Vector newLocation = _dragCanvasStartOffset - (((args.GetPosition(this) - _dragCanvasStartLocation)) * (1.0 / Scale)); CenterAt(newLocation); _movedDuringDrag = true; @@ -327,14 +374,18 @@ namespace Artemis.VisualScripting.Editor.Controls private void OnCanvasDragOver(object sender, DragEventArgs args) { - if (VisualScript == null) return; - if (VisualScript.IsConnecting) VisualScript.OnDragOver(args.GetPosition(_canvas)); } private void OnCanvasMouseWheel(object sender, MouseWheelEventArgs args) { + if (AutoFitScript) + { + args.Handled = true; + return; + } + if (args.Delta < 0) Scale /= ScaleFactor; else @@ -342,8 +393,6 @@ namespace Artemis.VisualScripting.Editor.Controls Scale = Clamp(Scale, MinScale, MaxScale); - _canvas.LayoutTransform = new ScaleTransform(Scale, Scale); - UpdatePanning(); args.Handled = true; @@ -373,9 +422,63 @@ namespace Artemis.VisualScripting.Editor.Controls if (d is not VisualScriptPresenter presenter) return; if (presenter.VisualScript == null) return; + presenter._canvasViewPortScale.ScaleX = presenter.Scale; + presenter._canvasViewPortScale.ScaleY = presenter.Scale; + presenter.VisualScript.NodeDragScale = 1.0 / presenter.Scale; } + private void FitScript() + { + if (_fitPending) return; + + _fitPending = true; + Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(FitScriptAction)); + } + + private void FitScriptAction() + { + _fitPending = false; + + if ((Script == null) || (_nodeList == null)) return; + + double minX = double.MaxValue; + double maxX = double.MinValue; + double minY = double.MaxValue; + double maxY = double.MinValue; + for (int i = 0; i < _nodeList.Items.Count; i++) + { + DependencyObject container = _nodeList.ItemContainerGenerator.ContainerFromIndex(i); + VisualScriptNodePresenter nodePresenter = GetChildOfType(container); + if (nodePresenter != null) + { + minX = Math.Min(minX, nodePresenter.Node.Node.X); + minY = Math.Min(minY, nodePresenter.Node.Node.Y); + + maxX = Math.Max(maxX, nodePresenter.Node.Node.X + nodePresenter.ActualWidth); + maxY = Math.Max(maxY, nodePresenter.Node.Node.Y + nodePresenter.ActualHeight); + } + } + + if (minX >= (double.MaxValue - 1)) + { + Scale = MaxScale; + CenterAt(new Vector(0, 0)); + } + else + { + double width = maxX - minX; + double height = maxY - minY; + + double scaleX = ActualWidth / width; + double scaleY = ActualHeight / height; + + Scale = Clamp(Math.Min(scaleX, scaleY) / 1.05, 0, MaxScale); //DarthAffe 21.08.2021: 5% Border + + CenterAt(new Vector(minX + (width / 2.0), minY + (height / 2.0))); + } + } + private void CreateNode(NodeData nodeData) { if (nodeData == null) return; @@ -385,21 +488,10 @@ namespace Artemis.VisualScripting.Editor.Controls INode node = nodeData.CreateNode(Script, null); node.Initialize(Script); + node.X = _lastRightClickLocation.X - VisualScript.LocationOffset; + node.Y = _lastRightClickLocation.Y - VisualScript.LocationOffset; + Script.AddNode(node); - - InitializeNode(node, _lastRightClickLocation); - } - - private void InitializeNode(INode node, Point? initialLocation = null) - { - VisualScriptNode visualScriptNode = new(VisualScript, node); - if (initialLocation != null) - { - visualScriptNode.X = initialLocation.Value.X; - visualScriptNode.Y = initialLocation.Value.Y; - } - visualScriptNode.SnapNodeToGrid(); - VisualScript.Nodes.Add(visualScriptNode); } private void CenterAt(Vector vector) @@ -414,8 +506,8 @@ namespace Artemis.VisualScripting.Editor.Controls if (_canvasViewPortTransform == null) return; double surfaceOffset = (SurfaceSize / 2.0) * Scale; - _canvasViewPortTransform.X = (((_viewportCenter.X * Scale) + (ActualWidth / 2.0))) - surfaceOffset; - _canvasViewPortTransform.Y = (((_viewportCenter.Y * Scale) + (ActualHeight / 2.0))) - surfaceOffset; + _canvasViewPortTransform.X = (((-_viewportCenter.X * Scale) + (ActualWidth / 2.0))) - surfaceOffset; + _canvasViewPortTransform.Y = (((-_viewportCenter.Y * Scale) + (ActualHeight / 2.0))) - surfaceOffset; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -436,6 +528,21 @@ namespace Artemis.VisualScripting.Editor.Controls return new Vector(x, y); } + public static T GetChildOfType(DependencyObject obj) + where T : DependencyObject + { + if (obj == null) return null; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++) + { + DependencyObject child = VisualTreeHelper.GetChild(obj, i); + + T result = (child as T) ?? GetChildOfType(child); + if (result != null) return result; + } + return null; + } + #endregion } } diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs index fa5ea0bd9..b63125a2b 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScript.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Windows; +using System.Windows.Threading; using Artemis.Core; using Artemis.VisualScripting.Events; using Artemis.VisualScripting.ViewModel; @@ -14,6 +16,8 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper { #region Properties & Fields + private bool _cableRecreationPending = false; + private readonly HashSet _selectedNodes = new(); private readonly Dictionary _nodeStartPositions = new(); private double _nodeDragAccumulationX; @@ -31,6 +35,8 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper set => SetProperty(ref _nodeDragScale, value); } + internal double LocationOffset { get; } + private VisualScriptPin _isConnectingPin; private VisualScriptPin IsConnectingPin { @@ -42,6 +48,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper } } + private readonly Dictionary _nodeMapping = new(); public ObservableCollection Nodes { get; } = new(); public IEnumerable Cables => Nodes.SelectMany(n => n.InputPins.SelectMany(p => p.InternalConnections)) @@ -54,6 +61,13 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper #endregion + #region Events + + public event EventHandler NodeMoved; + public event EventHandler NodeCollectionChanged; + + #endregion + #region Constructors public VisualScript(INodeScript script, int surfaceSize, int gridSize) @@ -62,7 +76,15 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper this.SurfaceSize = surfaceSize; this.GridSize = gridSize; + LocationOffset = SurfaceSize / 2.0; + Nodes.CollectionChanged += OnNodeCollectionChanged; + + script.NodeAdded += OnScriptNodeAdded; + script.NodeRemoved += OnScriptRemovedAdded; + + foreach (INode node in Script.Nodes) + InitializeNode(node); } #endregion @@ -85,31 +107,69 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper RegisterNodes(args.NewItems.Cast()); } + private void OnScriptNodeAdded(object sender, INode node) + { + if (_nodeMapping.ContainsKey(node)) return; + + InitializeNode(node); + } + + private void OnScriptRemovedAdded(object sender, INode node) + { + if (!_nodeMapping.TryGetValue(node, out VisualScriptNode visualScriptNode)) return; + + Nodes.Remove(visualScriptNode); + } + + private void InitializeNode(INode node) + { + VisualScriptNode visualScriptNode = new(this, node); + visualScriptNode.SnapNodeToGrid(); + Nodes.Add(visualScriptNode); + } + private void RegisterNodes(IEnumerable nodes) { foreach (VisualScriptNode node in nodes) { + _nodeMapping.Add(node.Node, node); + node.IsSelectedChanged += OnNodeIsSelectedChanged; node.DragStarting += OnNodeDragStarting; node.DragEnding += OnNodeDragEnding; node.DragMoving += OnNodeDragMoving; + node.PropertyChanged += OnNodePropertyChanged; if (node.IsSelected) _selectedNodes.Add(node); } + + NodeCollectionChanged?.Invoke(this, EventArgs.Empty); } private void UnregisterNodes(IEnumerable nodes) { foreach (VisualScriptNode node in nodes) { + _nodeMapping.Remove(node.Node); + node.IsSelectedChanged -= OnNodeIsSelectedChanged; node.DragStarting -= OnNodeDragStarting; node.DragEnding -= OnNodeDragEnding; node.DragMoving -= OnNodeDragMoving; + node.PropertyChanged -= OnNodePropertyChanged; _selectedNodes.Remove(node); } + + NodeCollectionChanged?.Invoke(this, EventArgs.Empty); + } + + private void OnNodePropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (string.Equals(args.PropertyName, nameof(VisualScriptNode.X), StringComparison.OrdinalIgnoreCase) + || string.Equals(args.PropertyName, nameof(VisualScriptNode.Y), StringComparison.OrdinalIgnoreCase)) + NodeMoved?.Invoke(this, EventArgs.Empty); } private void OnNodeIsSelectedChanged(object sender, VisualScriptNodeIsSelectedChangedEventArgs args) @@ -184,8 +244,18 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper Script.RemoveNode(node.Node); } + internal void RequestCableRecreation() + { + if (_cableRecreationPending) return; + + _cableRecreationPending = true; + Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(RecreateCables)); + } + internal void RecreateCables() { + _cableRecreationPending = false; + Dictionary pinMapping = Nodes.SelectMany(n => n.InputPins) .Concat(Nodes.SelectMany(n => n.OutputPins)) .Concat(Nodes.SelectMany(n => n.InputPinCollections.SelectMany(p => p.Pins))) @@ -200,13 +270,13 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper { foreach (IPin connectedPin in pin.ConnectedTo) if (!connectedPins.Contains(connectedPin)) - { - VisualScriptPin pin1 = pinMapping[pin]; - VisualScriptPin pin2 = pinMapping[connectedPin]; - VisualScriptCable cable = new(pin1, pin2, false); - pin1.InternalConnections.Add(cable); - pin2.InternalConnections.Add(cable); - } + if (pinMapping.TryGetValue(pin, out VisualScriptPin pin1) + && pinMapping.TryGetValue(connectedPin, out VisualScriptPin pin2)) + { + VisualScriptCable cable = new(pin1, pin2, false); + pin1.InternalConnections.Add(cable); + pin2.InternalConnections.Add(cable); + } connectedPins.Add(pin); } diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs index b69f56a01..778bdc0b9 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptNode.cs @@ -13,7 +13,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper { #region Properties & Fields - private double _locationOffset; + private readonly Dictionary _pinMapping = new(); public VisualScript Script { get; } public INode Node { get; } @@ -55,20 +55,20 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper public double X { - get => Node.X + _locationOffset; + get => Node.X + Script.LocationOffset; set { - Node.X = value - _locationOffset; + Node.X = value - Script.LocationOffset; OnPropertyChanged(); } } public double Y { - get => Node.Y + _locationOffset; + get => Node.Y + Script.LocationOffset; set { - Node.Y = value - _locationOffset; + Node.Y = value - Script.LocationOffset; OnPropertyChanged(); } } @@ -100,8 +100,6 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper Node.PropertyChanged += OnNodePropertyChanged; - _locationOffset = script.SurfaceSize / 2.0; - ValidatePins(); } @@ -114,6 +112,12 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper if (string.Equals(args.PropertyName, nameof(Node.Pins), StringComparison.OrdinalIgnoreCase) || string.Equals(args.PropertyName, nameof(Node.PinCollections), StringComparison.OrdinalIgnoreCase)) ValidatePins(); + + else if (string.Equals(args.PropertyName, nameof(Node.X), StringComparison.OrdinalIgnoreCase)) + OnPropertyChanged(nameof(X)); + + else if (string.Equals(args.PropertyName, nameof(Node.Y), StringComparison.OrdinalIgnoreCase)) + OnPropertyChanged(nameof(Y)); } private void ValidatePins() @@ -199,8 +203,30 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper } #endregion + + #region Pin Mapping + + _pinMapping.Clear(); + + foreach (VisualScriptPin pin in InputPins) + _pinMapping.Add(pin.Pin, pin); + + foreach (VisualScriptPin pin in OutputPins) + _pinMapping.Add(pin.Pin, pin); + + foreach (VisualScriptPinCollection pinCollection in InputPinCollections) + foreach (VisualScriptPin pin in pinCollection.Pins) + _pinMapping.Add(pin.Pin, pin); + + foreach (VisualScriptPinCollection pinCollection in OutputPinCollections) + foreach (VisualScriptPin pin in pinCollection.Pins) + _pinMapping.Add(pin.Pin, pin); + + #endregion } + internal VisualScriptPin GetVisualPin(IPin pin) => ((pin != null) && _pinMapping.TryGetValue(pin, out VisualScriptPin visualScriptPin)) ? visualScriptPin : null; + public void SnapNodeToGrid() { X -= X % Script.GridSize; @@ -219,8 +245,8 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper OnIsSelectedChanged(IsSelected, alterSelection); } - public void DragStart() => DragStarting?.Invoke(this, new EventArgs()); - public void DragEnd() => DragEnding?.Invoke(this, new EventArgs()); + public void DragStart() => DragStarting?.Invoke(this, EventArgs.Empty); + public void DragEnd() => DragEnding?.Invoke(this, EventArgs.Empty); public void DragMove(double dx, double dy) => DragMoving?.Invoke(this, new VisualScriptNodeDragMovingEventArgs(dx, dy)); private void OnIsSelectedChanged(bool isSelected, bool alterSelection) => IsSelectedChanged?.Invoke(this, new VisualScriptNodeIsSelectedChangedEventArgs(isSelected, alterSelection)); diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs index 933d8125e..a3720cbf4 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPin.cs @@ -19,6 +19,8 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper #region Properties & Fields + private bool _isConnectionUpdated = false; + private VisualScriptPin _isConnectingPin; private VisualScriptCable _isConnectingCable; @@ -39,8 +41,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper } } - public Point AbsoluteCableTargetPosition => Pin.Direction == PinDirection.Input ? new Point(AbsolutePosition.X - CABLE_OFFSET, AbsolutePosition.Y) - : new Point(AbsolutePosition.X + CABLE_OFFSET, AbsolutePosition.Y); + public Point AbsoluteCableTargetPosition => Pin.Direction == PinDirection.Input ? new Point(AbsolutePosition.X - CABLE_OFFSET, AbsolutePosition.Y) : new Point(AbsolutePosition.X + CABLE_OFFSET, AbsolutePosition.Y); #endregion @@ -50,12 +51,22 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper { this.Node = node; this.Pin = pin; + + pin.PinConnected += PinConnectionChanged; + pin.PinDisconnected += PinConnectionChanged; } #endregion #region Methods + private void PinConnectionChanged(object sender, IPin pin) + { + if (_isConnectionUpdated) return; + + Node?.Script?.RequestCableRecreation(); + } + public void SetConnecting(bool isConnecting) { if (isConnecting) @@ -63,8 +74,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper if (_isConnectingCable != null) SetConnecting(false); - _isConnectingPin = new VisualScriptPin(null, new IsConnectingPin(Pin.Direction == PinDirection.Input ? PinDirection.Output : PinDirection.Input, Pin.Type)) - { AbsolutePosition = AbsolutePosition }; + _isConnectingPin = new VisualScriptPin(null, new IsConnectingPin(Pin.Direction == PinDirection.Input ? PinDirection.Output : PinDirection.Input, Pin.Type)) { AbsolutePosition = AbsolutePosition }; _isConnectingCable = new VisualScriptCable(this, _isConnectingPin); Node.OnIsConnectingPinChanged(_isConnectingPin); } @@ -81,6 +91,8 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper { if (InternalConnections.Contains(cable)) return; + _isConnectionUpdated = true; + if (Pin.Direction == PinDirection.Input) { List cables = InternalConnections.ToList(); @@ -92,21 +104,31 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper Pin.ConnectTo(cable.GetConnectedPin(Pin)); Node?.OnPinConnected(new PinConnectedEventArgs(this, cable)); + + _isConnectionUpdated = false; } public void DisconnectAll() { + _isConnectionUpdated = true; + List cables = InternalConnections.ToList(); foreach (VisualScriptCable cable in cables) cable.Disconnect(); + + _isConnectionUpdated = false; } internal void Disconnect(VisualScriptCable cable) { + _isConnectionUpdated = true; + InternalConnections.Remove(cable); Pin.DisconnectFrom(cable.GetConnectedPin(Pin)); Node?.OnPinDisconnected(new PinDisconnectedEventArgs(this, cable)); + + _isConnectionUpdated = false; } #endregion diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs index 761542452..d9a1ac838 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs @@ -12,6 +12,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper public VisualScriptNode Node { get; } public IPinCollection PinCollection { get; } + private readonly Dictionary _pinMapping = new(); public ObservableCollection Pins { get; } = new(); #endregion @@ -33,27 +34,47 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper this.Node = node; this.PinCollection = pinCollection; + pinCollection.PinAdded += OnPinCollectionPinAdded; + pinCollection.PinRemoved += OnPinCollectionPinRemoved; + foreach (IPin pin in PinCollection) - Pins.Add(new VisualScriptPin(node, pin)); + { + VisualScriptPin visualScriptPin = new(node, pin); + _pinMapping.Add(pin, visualScriptPin); + Pins.Add(visualScriptPin); + } } #endregion #region Methods + private void OnPinCollectionPinRemoved(object sender, IPin pin) + { + if (!_pinMapping.TryGetValue(pin, out VisualScriptPin visualScriptPin)) return; + + visualScriptPin.DisconnectAll(); + Pins.Remove(visualScriptPin); + } + + private void OnPinCollectionPinAdded(object sender, IPin pin) + { + if (_pinMapping.ContainsKey(pin)) return; + + VisualScriptPin visualScriptPin = new(Node, pin); + _pinMapping.Add(pin, visualScriptPin); + Pins.Add(visualScriptPin); + } + public void AddPin() { - IPin pin = PinCollection.AddPin(); - Pins.Add(new VisualScriptPin(Node, pin)); + PinCollection.AddPin(); } public void RemovePin(VisualScriptPin pin) { - pin.DisconnectAll(); PinCollection.Remove(pin.Pin); - Pins.Remove(pin); } - public void RemoveAll() { List pins = new(Pins); From ed33d8a148cd53ab80c53616d985ada0f8175de2 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 22 Aug 2021 00:20:28 +0200 Subject: [PATCH 020/270] Nodes - When usting custom VMs, create a VM for each presenter --- src/Artemis.Core/Services/NodeService.cs | 5 +-- src/Artemis.Core/VisualScripting/Node.cs | 21 ++++++++--- .../Controls/VisualScriptNodePresenter.cs | 36 +++++++++++++++---- .../Styles/VisualScriptNodePresenter.xaml | 12 +------ 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 82cf2d4f7..5af2a659a 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -83,10 +83,7 @@ namespace Artemis.Core.Services // ignored } } - - if (node is CustomViewModelNode customViewModelNode) - customViewModelNode.BaseCustomViewModel = _kernel.Get(customViewModelNode.CustomViewModelType, new ConstructorArgument("node", node)); - + node.Initialize(script); return node; } diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index ac6b4da8a..4d1acc1eb 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -1,6 +1,8 @@ -using System; +using Ninject; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using Ninject.Parameters; namespace Artemis.Core { @@ -149,14 +151,24 @@ namespace Artemis.Core public abstract class Node : CustomViewModelNode where T : ICustomNodeViewModel { + [Inject] + internal IKernel Kernel { get; set; } = null!; + protected Node() { } protected Node(string name, string description) : base(name, description) { } - public override Type CustomViewModelType => typeof(T); - public T CustomViewModel => (T) BaseCustomViewModel!; + public virtual T GetViewModel() + { + return Kernel.Get(new ConstructorArgument("node", this)); + } + + public override ICustomNodeViewModel GetCustomViewModel() + { + return GetViewModel(); + } } public abstract class CustomViewModelNode : Node @@ -169,7 +181,6 @@ namespace Artemis.Core protected CustomViewModelNode(string name, string description) : base(name, description) { } - public abstract Type CustomViewModelType { get; } - public object? BaseCustomViewModel { get; set; } + public abstract ICustomNodeViewModel GetCustomViewModel(); } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs index e79a195de..cc88904df 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs @@ -3,6 +3,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; +using Artemis.Core; using Artemis.VisualScripting.Editor.Controls.Wrapper; namespace Artemis.VisualScripting.Editor.Controls @@ -21,20 +22,29 @@ namespace Artemis.VisualScripting.Editor.Controls #region Dependency Properties public static readonly DependencyProperty NodeProperty = DependencyProperty.Register( - "Node", typeof(VisualScriptNode), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(VisualScriptNode))); + "Node", typeof(VisualScriptNode), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(VisualScriptNode), NodePropertyChangedCallback)); public VisualScriptNode Node { - get => (VisualScriptNode)GetValue(NodeProperty); + get => (VisualScriptNode) GetValue(NodeProperty); set => SetValue(NodeProperty, value); } + public static readonly DependencyProperty CustomViewModelProperty = DependencyProperty.Register( + "CustomViewModel", typeof(ICustomNodeViewModel), typeof(VisualScriptNodePresenter), new PropertyMetadata(null)); + + public ICustomNodeViewModel CustomViewModel + { + get => (ICustomNodeViewModel) GetValue(CustomViewModelProperty); + set => SetValue(CustomViewModelProperty, value); + } + public static readonly DependencyProperty TitleBrushProperty = DependencyProperty.Register( "TitleBrush", typeof(Brush), typeof(VisualScriptNodePresenter), new PropertyMetadata(default(Brush))); public Brush TitleBrush { - get => (Brush)GetValue(TitleBrushProperty); + get => (Brush) GetValue(TitleBrushProperty); set => SetValue(TitleBrushProperty, value); } @@ -56,8 +66,8 @@ namespace Artemis.VisualScripting.Editor.Controls } Size neededSize = base.MeasureOverride(constraint); - int width = (int)Math.Ceiling(neededSize.Width); - int height = (int)Math.Ceiling(neededSize.Height); + int width = (int) Math.Ceiling(neededSize.Width); + int height = (int) Math.Ceiling(neededSize.Height); return new Size(SnapToGridSize(width), SnapToGridSize(height)); } @@ -132,6 +142,20 @@ namespace Artemis.VisualScripting.Editor.Controls args.Handled = true; } + private void GetCustomViewModel() + { + if (Node?.Node is CustomViewModelNode customViewModelNode) + CustomViewModel = customViewModelNode.GetCustomViewModel(); + else + CustomViewModel = null; + } + + private static void NodePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is VisualScriptNodePresenter presenter) + presenter.GetCustomViewModel(); + } + #endregion } -} +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml index c124dec3b..26d6ec27b 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptNodePresenter.xaml @@ -208,17 +208,7 @@ - - - - - - - - - - - + From 7933bf7ee999bbfba8b5a2a93c133384e29cfef5 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 22 Aug 2021 00:56:23 +0200 Subject: [PATCH 021/270] Nodes - Added to float node Nodes - Updated almost all custom VMs to stay in sync across multiple instances --- .../VisualScripting/CustomNodeViewModel.cs | 20 +++- .../Controls/VisualScriptNodePresenter.cs | 20 ++++ .../Nodes/ConvertNodes.cs | 80 ++++++++++++++-- .../CustomViewModels/CustomNodeViewModel.cs | 19 ++-- .../DataModelNodeCustomViewModel.cs | 24 ++--- .../LayerPropertyNodeCustomViewModel.cs | 10 +- .../StaticValueNodeViewModels.cs | 95 ++++++++++++++----- .../Nodes/DataModelNode.cs | 14 ++- 8 files changed, 217 insertions(+), 65 deletions(-) diff --git a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs index 5975ed806..7efc9956e 100644 --- a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs +++ b/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs @@ -1,9 +1,23 @@ -using Newtonsoft.Json; - -namespace Artemis.Core +namespace Artemis.Core { + /// + /// Represents a custom view model for a + /// public interface ICustomNodeViewModel { + /// + /// Gets the node the view models belongs to + /// public INode Node { get; } + + /// + /// Called whenever the custom view model is activated + /// + void OnActivate(); + + /// + /// Called whenever the custom view model is closed + /// + void OnDeactivate(); } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs index cc88904df..4532b9d8b 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs @@ -50,6 +50,15 @@ namespace Artemis.VisualScripting.Editor.Controls #endregion + #region Constructors + + public VisualScriptNodePresenter() + { + Unloaded += OnUnloaded; + } + + #endregion + #region Methods protected override Size MeasureOverride(Size constraint) @@ -144,8 +153,13 @@ namespace Artemis.VisualScripting.Editor.Controls private void GetCustomViewModel() { + CustomViewModel?.OnDeactivate(); + if (Node?.Node is CustomViewModelNode customViewModelNode) + { CustomViewModel = customViewModelNode.GetCustomViewModel(); + CustomViewModel.OnActivate(); + } else CustomViewModel = null; } @@ -156,6 +170,12 @@ namespace Artemis.VisualScripting.Editor.Controls presenter.GetCustomViewModel(); } + private void OnUnloaded(object sender, RoutedEventArgs e) + { + CustomViewModel?.OnDeactivate(); + CustomViewModel = null; + } + #endregion } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/ConvertNodes.cs b/src/Artemis.VisualScripting/Nodes/ConvertNodes.cs index f9d52f8dd..29215d8d1 100644 --- a/src/Artemis.VisualScripting/Nodes/ConvertNodes.cs +++ b/src/Artemis.VisualScripting/Nodes/ConvertNodes.cs @@ -60,10 +60,21 @@ namespace Artemis.VisualScripting.Nodes public override void Evaluate() { - if (!int.TryParse(Input.Value?.ToString(), out int value)) + Integer.Value = Input.Value switch + { + int input => input, + double input => (int) input, + float input => (int) input, + _ => TryParse(Input.Value) + }; + } + + private int TryParse(object input) + { + if (!int.TryParse(input?.ToString(), out int value)) value = 0; - Integer.Value = value; + return value; } #endregion @@ -95,12 +106,69 @@ namespace Artemis.VisualScripting.Nodes public override void Evaluate() { - if (!double.TryParse(Input.Value?.ToString(), out double value)) - value = 0; + Double.Value = Input.Value switch + { + int input => input, + double input => input, + float input => input, + _ => TryParse(Input.Value) + }; + } - Double.Value = value; + private double TryParse(object input) + { + if (!double.TryParse(input?.ToString(), out double value)) + value = 0.0; + + return value; } #endregion } -} + + [Node("To Float", "Converts the input to a float.")] + public class ConvertToFloatNode : Node + { + #region Properties & Fields + + public InputPin Input { get; } + + public OutputPin Float { get; } + + #endregion + + #region Constructors + + public ConvertToFloatNode() + : base("To Float", "Converts the input to a float.") + { + Input = CreateInputPin(); + Float = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Float.Value = Input.Value switch + { + int input => input, + double input => (float) input, + float input => input, + _ => TryParse(Input.Value) + }; + } + + private float TryParse(object input) + { + if (!float.TryParse(input?.ToString(), out float value)) + value = 0.0f; + + return value; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs index fca28dbbb..1c7d23809 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/CustomNodeViewModel.cs @@ -1,10 +1,9 @@ -using System.Windows; -using Artemis.Core; +using Artemis.Core; using Stylet; namespace Artemis.VisualScripting.Nodes.CustomViewModels { - public abstract class CustomNodeViewModel : PropertyChangedBase, IViewAware, ICustomNodeViewModel + public abstract class CustomNodeViewModel : PropertyChangedBase, ICustomNodeViewModel { protected CustomNodeViewModel(INode node) { @@ -13,21 +12,17 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels public INode Node { get; } - #region Implementation of IViewAware + #region Implementation of ICustomNodeViewModel /// - public void AttachView(UIElement view) - { - View = view; - OnDisplay(); - } - - protected virtual void OnDisplay() + public virtual void OnActivate() { } /// - public UIElement View { get; private set; } + public virtual void OnDeactivate() + { + } #endregion } diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs index 126a1815d..dccccb92a 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs @@ -1,4 +1,5 @@ -using Artemis.Core; +using System.ComponentModel; +using Artemis.Core; using Artemis.Core.Modules; using Stylet; @@ -26,7 +27,6 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels set { _node.DataModelPath = value; - OnPropertyChanged(nameof(DataModelPath)); if (_node.DataModelPath != null) { @@ -41,8 +41,8 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels _node.UpdateOutputPin(); } } - - public void Initialize() + + public override void OnActivate() { if (Modules != null) return; @@ -52,17 +52,19 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels Modules.Add(scriptProfile.Configuration.Module); else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) Modules.Add(profileConfiguration.Module); + + _node.PropertyChanged += NodeOnPropertyChanged; } - #region Overrides of CustomNodeViewModel - - /// - protected override void OnDisplay() + public override void OnDeactivate() { - Initialize(); - base.OnDisplay(); + _node.PropertyChanged -= NodeOnPropertyChanged; } - #endregion + private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(DataModelNode.DataModelPath)) + OnPropertyChanged(nameof(DataModelPath)); + } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs index edf84d7e1..05706be9a 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs @@ -66,15 +66,15 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels NotifyOfPropertyChange(nameof(SelectedLayerProperty)); } - #region Overrides of CustomNodeViewModel - - /// - protected override void OnDisplay() + public override void OnActivate() { GetProfileElements(); GetLayerProperties(); } - #endregion + public override void OnDeactivate() + { + base.OnDeactivate(); + } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs index 17b09f66e..82087eb87 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs @@ -1,4 +1,4 @@ -using Artemis.Core; +using System.ComponentModel; namespace Artemis.VisualScripting.Nodes.CustomViewModels { @@ -13,12 +13,24 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels public double Input { - get => (double) _node.Storage; - set - { - _node.Storage = value; + get => (double) (_node.Storage ?? 0.0); + set => _node.Storage = value; + } + + public override void OnActivate() + { + _node.PropertyChanged += NodeOnPropertyChanged; + } + + public override void OnDeactivate() + { + _node.PropertyChanged -= NodeOnPropertyChanged; + } + + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Node.Storage)) OnPropertyChanged(nameof(Input)); - } } } @@ -33,12 +45,24 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels public float Input { - get => (float)_node.Storage; - set - { - _node.Storage = value; + get => (float) (_node.Storage ?? 0f); + set => _node.Storage = value; + } + + public override void OnActivate() + { + _node.PropertyChanged += NodeOnPropertyChanged; + } + + public override void OnDeactivate() + { + _node.PropertyChanged -= NodeOnPropertyChanged; + } + + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Node.Storage)) OnPropertyChanged(nameof(Input)); - } } } @@ -53,17 +77,24 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels public int Input { - get - { - if (_node.Storage is long longInput) - return (int)longInput; - return (int)_node.Storage; - } - set - { - _node.Storage = value; + get => _node.Storage is long longInput ? (int) longInput : (int) (_node.Storage ?? 0); + set => _node.Storage = value; + } + + public override void OnActivate() + { + _node.PropertyChanged += NodeOnPropertyChanged; + } + + public override void OnDeactivate() + { + _node.PropertyChanged -= NodeOnPropertyChanged; + } + + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Node.Storage)) OnPropertyChanged(nameof(Input)); - } } } @@ -78,12 +109,24 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels public string Input { - get => (string)_node.Storage; - set - { - _node.Storage = value; + get => (string) _node.Storage; + set => _node.Storage = value; + } + + public override void OnActivate() + { + _node.PropertyChanged += NodeOnPropertyChanged; + } + + public override void OnDeactivate() + { + _node.PropertyChanged -= NodeOnPropertyChanged; + } + + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Node.Storage)) OnPropertyChanged(nameof(Input)); - } } } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs index 64b28a169..4f7a825c4 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs +++ b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs @@ -9,13 +9,20 @@ namespace Artemis.VisualScripting.Nodes [Node("Data Model-Value", "Outputs a selectable data model value.")] public class DataModelNode : Node { + private DataModelPath _dataModelPath; + public DataModelNode() : base("Data Model", "Outputs a selectable data model value") { } public INodeScript Script { get; private set; } public OutputPin Output { get; private set; } - public DataModelPath DataModelPath { get; set; } + + public DataModelPath DataModelPath + { + get => _dataModelPath; + set => SetAndNotify(ref _dataModelPath , value); + } public override void Initialize(INodeScript script) { @@ -36,7 +43,10 @@ namespace Artemis.VisualScripting.Nodes public void UpdateOutputPin() { - if (Pins.Contains(Output)) + if (Output != null && Output.Type == DataModelPath?.GetPropertyType()) + return; + + if (Output != null && Pins.Contains(Output)) { RemovePin(Output); Output = null; From 0bb93f2ebd242b7935ed8d2027fd8b54c6548975 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 22 Aug 2021 18:17:05 +0200 Subject: [PATCH 022/270] Data bindings - Rewrote data binding system to fully leverage nodes --- src/Artemis.Core/Artemis.Core.csproj | 4 + .../Converters/FloatDataBindingConverter.cs | 47 --- .../Converters/GeneralDataBindingConverter.cs | 33 -- .../Converters/IntDataBindingConverter.cs | 41 -- .../Converters/SKColorDataBindingConverter.cs | 29 -- .../Properties/BoolLayerProperty.cs | 7 +- .../Properties/ColorGradientLayerProperty.cs | 28 +- .../Properties/EnumLayerProperty.cs | 1 - .../Properties/FloatLayerProperty.cs | 7 +- .../Properties/FloatRangeLayerProperty.cs | 10 +- .../Properties/IntLayerProperty.cs | 7 +- .../Properties/IntRangeLayerProperty.cs | 10 +- .../LayerBrushReferenceLayerProperty.cs | 1 - .../Properties/SKColorLayerProperty.cs | 7 +- .../Properties/SKPointLayerProperty.cs | 9 +- .../Properties/SKSizeLayerProperty.cs | 9 +- .../Events/Profiles/DataBindingEventArgs.cs | 20 + .../Profile/DataBindings/DataBinding.cs | 261 ++++++------- .../DataBindings/DataBindingConverter.cs | 90 ----- .../DataBindings/DataBindingProperty.cs | 46 +++ .../DataBindings/DataBindingRegistration.cs | 87 ----- .../Profile/DataBindings/IDataBinding.cs | 42 ++- .../DataBindings/IDataBindingConverter.cs | 16 - ...egistration.cs => IDataBindingProperty.cs} | 26 +- .../Profile/LayerProperties/ILayerProperty.cs | 47 ++- .../Profile/LayerProperties/LayerProperty.cs | 352 +++++------------- .../Models/Profile/RenderProfileElement.cs | 7 +- .../VisualScripting/DataBindingNodeScript.cs | 36 ++ src/Artemis.Core/VisualScripting/InputPin.cs | 2 +- .../Internal/DataBindingExitNode.cs | 73 ++++ .../Profile/DataBindings/DataBindingEntity.cs | 6 +- .../Entities/Profile/PropertyEntity.cs | 3 +- .../PropertyInput/PropertyInputViewModel.cs | 16 +- .../Interfaces/IProfileEditorService.cs | 14 +- .../Services/ProfileEditorService.cs | 8 +- .../BoolPropertyInputViewModel.cs | 8 +- .../PropertyInput/BrushPropertyInputView.xaml | 1 - .../BrushPropertyInputViewModel.cs | 4 +- .../FloatPropertyInputViewModel.cs | 10 - .../FloatRangePropertyInputView.xaml | 2 +- .../FloatRangePropertyInputViewModel.cs | 13 - .../IntPropertyInputViewModel.cs | 10 - .../IntRangePropertyInputView.xaml | 4 +- .../IntRangePropertyInputViewModel.cs | 13 - .../SKColorPropertyInputView.xaml | 4 +- .../SKColorPropertyInputViewModel.cs | 10 - .../SKPointPropertyInputView.xaml | 4 +- .../SKPointPropertyInputViewModel.cs | 13 - .../SKSizePropertyInputView.xaml | 4 +- .../SKSizePropertyInputViewModel.cs | 13 - .../Ninject/Factories/IVMFactory.cs | 5 - .../DataBindingsViewModelInstanceProvider.cs | 26 -- src/Artemis.UI/Ninject/UiModule.cs | 1 - .../DataBindings/DataBindingView.xaml | 167 --------- .../DataBindings/DataBindingView.xaml.cs | 15 - .../DataBindings/DataBindingViewModel.cs | 251 ------------- .../DataBindings/DataBindingsView.xaml | 58 ++- .../DataBindings/DataBindingsViewModel.cs | 83 ++--- .../LayerProperties/LayerPropertiesView.xaml | 1 - .../Tree/TreePropertyViewModel.cs | 16 +- .../Visualization/ProfileViewModel.cs | 39 +- .../LayerPropertyNodeCustomViewModel.cs | 2 +- .../Nodes/LayerPropertyNode.cs | 8 +- 63 files changed, 654 insertions(+), 1533 deletions(-) delete mode 100644 src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs delete mode 100644 src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs delete mode 100644 src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs delete mode 100644 src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs create mode 100644 src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs delete mode 100644 src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs create mode 100644 src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs delete mode 100644 src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs delete mode 100644 src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs rename src/Artemis.Core/Models/Profile/DataBindings/{IDataBindingRegistration.cs => IDataBindingProperty.cs} (55%) create mode 100644 src/Artemis.Core/VisualScripting/DataBindingNodeScript.cs create mode 100644 src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs delete mode 100644 src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 6e416d8dd..4cb259807 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -84,4 +84,8 @@ PreserveNewest + + + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs deleted file mode 100644 index 75818ac95..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - public class FloatDataBindingConverter : FloatDataBindingConverter - { - } - - /// - /// The type of layer property this converter is applied to - public class FloatDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public FloatDataBindingConverter() - { - SupportsSum = true; - SupportsInterpolate = true; - } - - /// - public override float Sum(float a, float b) - { - return a + b; - } - - /// - public override float Interpolate(float a, float b, double progress) - { - float diff = b - a; - return (float) (a + diff * progress); - } - - /// - public override void ApplyValue(float value) - { - if (DataBinding!.LayerProperty.PropertyDescription.MaxInputValue is float max) - value = Math.Min(value, max); - if (DataBinding!.LayerProperty.PropertyDescription.MinInputValue is float min) - value = Math.Max(value, min); - - base.ApplyValue(value); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs deleted file mode 100644 index 584967a76..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a generic data binding converter that acts as the bridge between a - /// and a and does not support - /// sum or interpolation - /// - public class GeneralDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public GeneralDataBindingConverter() - { - SupportsSum = false; - SupportsInterpolate = false; - } - - /// - public override T Sum(T a, T b) - { - throw new NotSupportedException(); - } - - /// - public override T Interpolate(T a, T b, double progress) - { - throw new NotSupportedException(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs deleted file mode 100644 index 795145630..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - public class IntDataBindingConverter : IntDataBindingConverter - { - } - - /// - public class IntDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public IntDataBindingConverter() - { - SupportsSum = true; - SupportsInterpolate = true; - } - - /// - /// Gets or sets the mode used for rounding during interpolation. Defaults to - /// - /// - public MidpointRounding InterpolationRoundingMode { get; set; } = MidpointRounding.AwayFromZero; - - /// - public override int Sum(int a, int b) - { - return a + b; - } - - /// - public override int Interpolate(int a, int b, double progress) - { - int diff = b - a; - return (int) Math.Round(a + diff * progress, InterpolationRoundingMode); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs deleted file mode 100644 index 6b708e088..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - /// - public class SKColorDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public SKColorDataBindingConverter() - { - SupportsInterpolate = true; - SupportsSum = true; - } - - /// - public override SKColor Sum(SKColor a, SKColor b) - { - return a.Sum(b); - } - - /// - public override SKColor Interpolate(SKColor a, SKColor b, double progress) - { - return a.Interpolate(b, (float) progress); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs index 99a7b614a..ccaecf8b8 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs @@ -4,9 +4,14 @@ public class BoolLayerProperty : LayerProperty { internal BoolLayerProperty() + { + } + + /// + protected override void OnInitialize() { KeyframesSupported = false; - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new GeneralDataBindingConverter(), "Value"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs index e811a4e0f..75d7c8a61 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs @@ -12,7 +12,6 @@ namespace Artemis.Core internal ColorGradientLayerProperty() { KeyframesSupported = false; - DataBindingsSupported = true; DefaultValue = new ColorGradient(); CurrentValueSet += OnCurrentValueSet; @@ -20,7 +19,7 @@ namespace Artemis.Core private void CreateDataBindingRegistrations() { - ClearDataBindingProperties(); + DataBinding.ClearDataBindingProperties(); if (CurrentValue == null) return; @@ -33,7 +32,7 @@ namespace Artemis.Core CurrentValue[stopIndex].Color = value; } - RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, new ColorStopDataBindingConverter(), $"Color #{stopIndex + 1}"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, $"Color #{stopIndex + 1}"); } } @@ -71,7 +70,7 @@ namespace Artemis.Core private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args) { - if (CurrentValue.Count != GetAllDataBindingRegistrations().Count) + if (CurrentValue.Count != DataBinding.Properties.Count) CreateDataBindingRegistrations(); } @@ -89,25 +88,4 @@ namespace Artemis.Core #endregion } - - internal class ColorStopDataBindingConverter : DataBindingConverter - { - public ColorStopDataBindingConverter() - { - SupportsInterpolate = true; - SupportsSum = true; - } - - /// - public override SKColor Sum(SKColor a, SKColor b) - { - return a.Sum(b); - } - - /// - public override SKColor Interpolate(SKColor a, SKColor b, double progress) - { - return a.Interpolate(b, (float) progress); - } - } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs index eb99a3b3e..ea7962b7a 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs @@ -8,7 +8,6 @@ namespace Artemis.Core internal EnumLayerProperty() { KeyframesSupported = false; - DataBindingsSupported = false; } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs index cb1a6eae8..2aec47126 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs @@ -5,7 +5,12 @@ { internal FloatLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new FloatDataBindingConverter(), "Value"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs index 34a3be646..d43dbf705 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs @@ -5,13 +5,17 @@ { internal FloatRangeLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue.Start = value, new FloatDataBindingConverter(), "Start"); - RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue.End = value, new FloatDataBindingConverter(), "End"); - CurrentValueSet += OnCurrentValueSet; DefaultValue = new FloatRange(); } + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue.Start = value, "Start"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue.End = value, "End"); + } + /// protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { diff --git a/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs index f69a1bdbc..03681df72 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs @@ -7,7 +7,12 @@ namespace Artemis.Core { internal IntLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new IntDataBindingConverter(), "Value"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs index 0b1942356..fea761b8e 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs @@ -5,13 +5,17 @@ { internal IntRangeLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue.Start = value, new IntDataBindingConverter(), "Start"); - RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue.End = value, new IntDataBindingConverter(), "End"); - CurrentValueSet += OnCurrentValueSet; DefaultValue = new IntRange(); } + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue.Start = value, "Start"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue.End = value, "End"); + } + /// protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { diff --git a/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs index 9bbd77eac..c3341ee32 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs @@ -8,7 +8,6 @@ internal LayerBrushReferenceLayerProperty() { KeyframesSupported = false; - DataBindingsSupported = false; } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs index 46370568a..38caad133 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs @@ -7,7 +7,12 @@ namespace Artemis.Core { internal SKColorLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new SKColorDataBindingConverter(), "Value"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs index 3cd654ad9..df9517eeb 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs @@ -7,8 +7,13 @@ namespace Artemis.Core { internal SKPointLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue.X, value => CurrentValue = new SKPoint(value, CurrentValue.Y), new FloatDataBindingConverter(), "X"); - RegisterDataBindingProperty(() => CurrentValue.Y, value => CurrentValue = new SKPoint(CurrentValue.X, value), new FloatDataBindingConverter(), "Y"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue.X, value => CurrentValue = new SKPoint(value, CurrentValue.Y), "X"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Y, value => CurrentValue = new SKPoint(CurrentValue.X, value), "Y"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs index 3274c26f9..e8344e0c9 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs @@ -7,8 +7,13 @@ namespace Artemis.Core { internal SKSizeLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue.Width, (value) => CurrentValue = new SKSize(value, CurrentValue.Height), new FloatDataBindingConverter(), "Width"); - RegisterDataBindingProperty(() => CurrentValue.Height, (value) => CurrentValue = new SKSize(CurrentValue.Width, value), new FloatDataBindingConverter(), "Height"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Width, (value) => CurrentValue = new SKSize(value, CurrentValue.Height), "Width"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Height, (value) => CurrentValue = new SKSize(CurrentValue.Width, value), "Height"); } /// diff --git a/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs b/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs new file mode 100644 index 000000000..c01cfae65 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data for data binding events. + /// + public class DataBindingEventArgs : EventArgs + { + internal DataBindingEventArgs(IDataBinding dataBinding) + { + DataBinding = dataBinding; + } + + /// + /// Gets the data binding this event is related to + /// + public IDataBinding DataBinding { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index e2392c79e..e02908256 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -1,25 +1,24 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Artemis.Storage.Entities.Profile.DataBindings; namespace Artemis.Core { /// - public class DataBinding : IDataBinding + public class DataBinding : IDataBinding { - private TProperty _currentValue = default!; + private readonly List _properties = new(); private bool _disposed; - private TimeSpan _easingProgress; - private TProperty _lastAppliedValue = default!; - private TProperty _previousValue = default!; - private bool _reapplyValue; + private bool _isEnabled; - internal DataBinding(DataBindingRegistration dataBindingRegistration) + internal DataBinding(LayerProperty layerProperty) { - LayerProperty = dataBindingRegistration.LayerProperty; - Entity = new DataBindingEntity(); + LayerProperty = layerProperty; - ApplyRegistration(dataBindingRegistration); - Script = new NodeScript(GetScriptName(), "The value to put into the data binding", LayerProperty.ProfileElement.Profile); + Entity = new DataBindingEntity(); + Script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); Save(); } @@ -27,39 +26,23 @@ namespace Artemis.Core internal DataBinding(LayerProperty layerProperty, DataBindingEntity entity) { LayerProperty = layerProperty; + Entity = entity; - Script = new NodeScript(GetScriptName(), "The value to put into the data binding", LayerProperty.ProfileElement.Profile); + Script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); // Load will add children so be initialized before that Load(); } - /// - /// Gets the data binding registration this data binding is based upon - /// - public DataBindingRegistration? Registration { get; private set; } - /// /// Gets the layer property this data binding targets /// public LayerProperty LayerProperty { get; } /// - /// Gets the converter used to apply this data binding to the + /// Gets the script used to populate the data binding /// - public DataBindingConverter? Converter { get; private set; } - - public NodeScript Script { get; private set; } - - /// - /// Gets or sets the easing time of the data binding - /// - public TimeSpan EasingTime { get; set; } - - /// - /// Gets ors ets the easing function of the data binding - /// - public Easings.Functions EasingFunction { get; set; } + public DataBindingNodeScript Script { get; private set; } /// /// Gets the data binding entity this data binding uses for persistent storage @@ -67,38 +50,52 @@ namespace Artemis.Core public DataBindingEntity Entity { get; } /// - /// Gets the current value of the data binding + /// Updates the pending values of this data binding /// - /// The base value of the property the data binding is applied to - /// - public TProperty GetValue(TProperty baseValue) + public void Update() { if (_disposed) throw new ObjectDisposedException("DataBinding"); - if (Converter == null) - return baseValue; + if (!IsEnabled) + return; + + // TODO: Update the base node Script.Run(); - - // If no easing is to be applied simple return whatever the current value is - if (EasingTime == TimeSpan.Zero || !Converter.SupportsInterpolate) - return Script.Result; - - // If the value changed, update the current and previous values used for easing - if (!Equals(Script.Result, _currentValue)) - ResetEasing(Script.Result); - - // Apply interpolation between the previous and current value - return GetInterpolatedValue(); } /// - /// Returns the type of the target property of this data binding + /// Registers a data binding property so that is available to the data binding system /// - public Type? GetTargetType() + /// The type of the layer property + /// The function to call to get the value of the property + /// The action to call to set the value of the property + /// The display name of the data binding property + public DataBindingProperty RegisterDataBindingProperty(Func getter, Action setter, string displayName) { - return Registration?.Getter.Method.ReturnType; + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + if (Properties.Any(d => d.DisplayName == displayName)) + throw new ArtemisCoreException($"A databinding property named '{displayName}' is already registered."); + + DataBindingProperty property = new(getter, setter, displayName); + _properties.Add(property); + + OnDataBindingPropertyRegistered(); + return property; + } + + /// + /// Removes all data binding properties so they are no longer available to the data binding system + /// + public void ClearDataBindingProperties() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + _properties.Clear(); + OnDataBindingPropertiesCleared(); } /// @@ -113,111 +110,78 @@ namespace Artemis.Core if (disposing) { _disposed = true; - - if (Registration != null) - Registration.DataBinding = null; - Script.Dispose(); } } - private void ResetEasing(TProperty value) + /// + /// Invokes the event + /// + protected virtual void OnDataBindingPropertyRegistered() { - _previousValue = GetInterpolatedValue(); - _currentValue = value; - _easingProgress = TimeSpan.Zero; - } - - private void ApplyRegistration(DataBindingRegistration dataBindingRegistration) - { - if (dataBindingRegistration == null) - throw new ArgumentNullException(nameof(dataBindingRegistration)); - - dataBindingRegistration.DataBinding = this; - Converter = dataBindingRegistration.Converter; - Registration = dataBindingRegistration; - - if (GetTargetType()!.IsValueType) - { - if (_currentValue == null) - _currentValue = default!; - if (_previousValue == null) - _previousValue = default!; - } - - Converter?.Initialize(this); - } - - private TProperty GetInterpolatedValue() - { - if (_easingProgress == EasingTime || Converter == null || !Converter.SupportsInterpolate) - return _currentValue; - - double easingAmount = _easingProgress.TotalSeconds / EasingTime.TotalSeconds; - return Converter.Interpolate(_previousValue, _currentValue, Easings.Interpolate(easingAmount, EasingFunction)); + DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this)); } /// - /// Updates the smoothing progress of the data binding + /// Invokes the event /// - /// The timeline to apply during update - public void Update(Timeline timeline) + protected virtual void OnDataBindingPropertiesCleared() { - // Don't update data bindings if there is no delta, otherwise this creates an inconsistency between - // data bindings with easing and data bindings without easing (the ones with easing will seemingly not update) - if (timeline.Delta == TimeSpan.Zero || timeline.IsOverridden) - return; + DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this)); + } - UpdateWithDelta(timeline.Delta); + /// + /// Invokes the event + /// + protected virtual void OnDataBindingEnabled(DataBindingEventArgs e) + { + DataBindingEnabled?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingDisabled(DataBindingEventArgs e) + { + DataBindingDisabled?.Invoke(this, e); + } + + private string GetScriptName() + { + return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path; } /// - public void UpdateWithDelta(TimeSpan delta) + public ILayerProperty BaseLayerProperty => LayerProperty; + + /// + public bool IsEnabled { - if (_disposed) - throw new ObjectDisposedException("DataBinding"); + get => _isEnabled; + set + { + _isEnabled = value; - // Data bindings cannot go back in time like brushes - if (delta < TimeSpan.Zero) - delta = TimeSpan.Zero; - - _easingProgress = _easingProgress.Add(delta); - if (_easingProgress > EasingTime) - _easingProgress = EasingTime; - - // Tell Apply() to apply a new value next call - _reapplyValue = false; + if (_isEnabled) + OnDataBindingEnabled(new DataBindingEventArgs(this)); + else + OnDataBindingDisabled(new DataBindingEventArgs(this)); + } } + /// + public ReadOnlyCollection Properties => _properties.AsReadOnly(); + /// public void Apply() { if (_disposed) throw new ObjectDisposedException("DataBinding"); - if (Converter == null) + if (!IsEnabled) return; - // If Update() has not been called, reapply the previous value - if (_reapplyValue) - { - Converter.ApplyValue(_lastAppliedValue); - return; - } - - TProperty converterValue = Converter.GetValue(); - TProperty value = GetValue(converterValue); - Converter.ApplyValue(value); - - _lastAppliedValue = value; - _reapplyValue = true; - } - - private string GetScriptName() - { - if (LayerProperty.GetAllDataBindingRegistrations().Count == 1) - return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path; - return $"{LayerProperty.PropertyDescription.Name ?? LayerProperty.Path} - {Registration?.DisplayName}"; + Script.DataBindingExitNode.ApplyToDataBinding(); } /// @@ -227,6 +191,19 @@ namespace Artemis.Core GC.SuppressFinalize(this); } + /// + public event EventHandler? DataBindingPropertyRegistered; + + /// + public event EventHandler? DataBindingPropertiesCleared; + + /// + public event EventHandler? DataBindingEnabled; + + /// + public event EventHandler? DataBindingDisabled; + + #region Storage /// @@ -234,14 +211,6 @@ namespace Artemis.Core { if (_disposed) throw new ObjectDisposedException("DataBinding"); - - // General - DataBindingRegistration? registration = LayerProperty.GetDataBindingRegistration(Entity.Identifier); - if (registration != null) - ApplyRegistration(registration); - - EasingTime = Entity.EasingTime; - EasingFunction = (Easings.Functions) Entity.EasingFunction; } /// @@ -249,8 +218,8 @@ namespace Artemis.Core { Script.Dispose(); Script = Entity.NodeScript != null - ? new NodeScript(GetScriptName(), "The value to put into the data binding", Entity.NodeScript, LayerProperty.ProfileElement.Profile) - : new NodeScript(GetScriptName(), "The value to put into the data binding", LayerProperty.ProfileElement.Profile); + ? new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile) + : new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); } /// @@ -259,17 +228,7 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("DataBinding"); - if (!LayerProperty.Entity.DataBindingEntities.Contains(Entity)) - LayerProperty.Entity.DataBindingEntities.Add(Entity); - - // Don't save an invalid state - if (Registration != null) - Entity.Identifier = Registration.DisplayName; - - Entity.EasingTime = EasingTime; - Entity.EasingFunction = (int) EasingFunction; - - Script?.Save(); + Script.Save(); Entity.NodeScript = Script?.Entity; } diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs deleted file mode 100644 index 2b3d7eb5a..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a data binding converter that acts as the bridge between a - /// and a - /// - public abstract class DataBindingConverter : IDataBindingConverter - { - /// - /// Gets the data binding this converter is applied to - /// - public DataBinding? DataBinding { get; private set; } - - /// - /// Gets whether or not this data binding converter supports the method - /// - public bool SupportsSum { get; protected set; } - - /// - /// Gets whether or not this data binding converter supports the method - /// - public bool SupportsInterpolate { get; protected set; } - - /// - /// Returns the sum of and - /// - public abstract TProperty Sum(TProperty a, TProperty b); - - /// - /// Returns the the interpolated value between and on a scale (generally) - /// between 0.0 and 1.0 defined by the - /// Note: The progress may go be negative or go beyond 1.0 depending on the easing method used - /// - /// The value to interpolate away from - /// The value to interpolate towards - /// The progress of the interpolation between 0.0 and 1.0 - /// - public abstract TProperty Interpolate(TProperty a, TProperty b, double progress); - - /// - /// Applies the to the layer property - /// - /// - public virtual void ApplyValue(TProperty value) - { - if (DataBinding?.Registration == null) - throw new ArtemisCoreException("Data binding converter is not yet initialized"); - DataBinding.Registration.Setter(value); - } - - /// - /// Returns the current base value of the data binding - /// - public virtual TProperty GetValue() - { - if (DataBinding?.Registration == null) - throw new ArtemisCoreException("Data binding converter is not yet initialized"); - return DataBinding.Registration.Getter(); - } - - /// - /// Converts the provided object to a type of - /// - public virtual TProperty ConvertFromObject(object? source) - { - return (TProperty) Convert.ChangeType(source, typeof(TProperty))!; - } - - /// - /// Called when the data binding converter has been initialized and the is available - /// - protected virtual void OnInitialized() - { - } - - internal void Initialize(DataBinding dataBinding) - { - if (dataBinding.Registration == null) - throw new ArtemisCoreException("Cannot initialize a data binding converter for a data binding without a registration"); - - DataBinding = dataBinding; - OnInitialized(); - } - - /// - public Type SupportedType => typeof(TProperty); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs new file mode 100644 index 000000000..6c67300a2 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs @@ -0,0 +1,46 @@ +using System; + +namespace Artemis.Core +{ + /// + public class DataBindingProperty : IDataBindingProperty + { + internal DataBindingProperty(Func getter, Action setter, string displayName) + { + Getter = getter ?? throw new ArgumentNullException(nameof(getter)); + Setter = setter ?? throw new ArgumentNullException(nameof(setter)); + DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); + } + + /// + /// Gets the function to call to get the value of the property + /// + public Func Getter { get; } + + /// + /// Gets the action to call to set the value of the property + /// + public Action Setter { get; } + + /// + public string DisplayName { get; } + + /// + public Type ValueType => typeof(TProperty); + + /// + public object? GetValue() + { + return Getter(); + } + + /// + public void SetValue(object? value) + { + if (value is TProperty match) + Setter(match); + else + throw new ArgumentException("Value must match the type of the data binding registration", nameof(value)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs deleted file mode 100644 index e7d5ca1d1..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Linq; -using Artemis.Storage.Entities.Profile.DataBindings; - -namespace Artemis.Core -{ - /// - public class DataBindingRegistration : IDataBindingRegistration - { - internal DataBindingRegistration(LayerProperty layerProperty, DataBindingConverter converter, - Func getter, Action setter, string displayName) - { - LayerProperty = layerProperty ?? throw new ArgumentNullException(nameof(layerProperty)); - Converter = converter ?? throw new ArgumentNullException(nameof(converter)); - Getter = getter ?? throw new ArgumentNullException(nameof(getter)); - Setter = setter ?? throw new ArgumentNullException(nameof(setter)); - DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); - } - - /// - /// Gets the layer property this registration was made on - /// - public LayerProperty LayerProperty { get; } - - /// - /// Gets the converter that's used by the data binding - /// - public DataBindingConverter Converter { get; } - - /// - /// Gets the function to call to get the value of the property - /// - public Func Getter { get; } - - /// - /// Gets the action to call to set the value of the property - /// - public Action Setter { get; } - - /// - public string DisplayName { get; } - - /// - public Type ValueType => typeof(TProperty); - - /// - /// Gets the data binding created using this registration - /// - public DataBinding? DataBinding { get; internal set; } - - /// - public IDataBinding? GetDataBinding() - { - return DataBinding; - } - - /// - public IDataBinding? CreateDataBinding() - { - if (DataBinding != null) - return DataBinding; - - DataBindingEntity? dataBinding = LayerProperty.Entity.DataBindingEntities.FirstOrDefault(e => e.Identifier == DisplayName); - if (dataBinding == null) - return null; - - DataBinding = new DataBinding(LayerProperty, dataBinding); - return DataBinding; - } - - /// - public void ClearDataBinding() - { - if (DataBinding == null) - return; - - // The related entity is left behind, just in case the data binding is added back later - LayerProperty.DisableDataBinding(DataBinding); - } - - /// - public object? GetValue() - { - return Getter(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs index 3c7c995bd..e706a3701 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.ObjectModel; using Artemis.Core.Modules; namespace Artemis.Core @@ -7,22 +8,51 @@ namespace Artemis.Core /// Represents a data binding that binds a certain to a value inside a /// /// - public interface IDataBinding : IStorageModel, IUpdateModel, IDisposable + public interface IDataBinding : IStorageModel, IDisposable { /// - /// Updates the smoothing progress of the data binding and recalculates the value next call + /// Gets the layer property the data binding is applied to /// - /// The delta to apply during update - void UpdateWithDelta(TimeSpan delta); + ILayerProperty BaseLayerProperty { get; } /// - /// Applies the data binding to the layer property + /// Gets a list of sub-properties this data binding applies to + /// + ReadOnlyCollection Properties { get; } + + /// + /// Gets a boolean indicating whether the data binding is enabled or not + /// + bool IsEnabled { get; set; } + + /// + /// Applies the pending value of the data binding to the property /// void Apply(); - + /// /// If the data binding is enabled, loads the node script for that data binding /// void LoadNodeScript(); + + /// + /// Occurs when a data binding property has been added + /// + public event EventHandler? DataBindingPropertyRegistered; + + /// + /// Occurs when all data binding properties have been removed + /// + public event EventHandler? DataBindingPropertiesCleared; + + /// + /// Occurs when a data binding has been enabled + /// + public event EventHandler? DataBindingEnabled; + + /// + /// Occurs when a data binding has been disabled + /// + public event EventHandler? DataBindingDisabled; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs deleted file mode 100644 index 9f207133f..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a data binding converter that acts as the bridge between a - /// and a - /// - public interface IDataBindingConverter - { - /// - /// Gets the type this converter supports - /// - public Type SupportedType { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs similarity index 55% rename from src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs rename to src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs index 4b9ceeb94..c6f299766 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs @@ -5,7 +5,7 @@ namespace Artemis.Core /// /// Represents a data binding registration /// - public interface IDataBindingRegistration + public interface IDataBindingProperty { /// /// Gets or sets the display name of the data binding registration @@ -18,25 +18,15 @@ namespace Artemis.Core Type ValueType { get; } /// - /// Returns the data binding applied using this registration - /// - public IDataBinding? GetDataBinding(); - - /// - /// If found, creates a data binding from storage - /// - /// - IDataBinding? CreateDataBinding(); - - /// - /// If present, removes the current data binding - /// - void ClearDataBinding(); - - /// - /// Gets the value of the data binding + /// Gets the value of the property this registration points to /// /// A value matching the type of object? GetValue(); + + /// + /// Sets the value of the property this registration points to + /// + /// A value matching the type of + void SetValue(object? value); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index dcfcdeeda..9391c1027 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -23,6 +22,21 @@ namespace Artemis.Core /// LayerPropertyGroup LayerPropertyGroup { get; } + /// + /// Gets the data binding of this property + /// + IDataBinding BaseDataBinding { get; } + + /// + /// Gets a boolean indicating whether the layer has any data binding properties + /// + public bool HasDataBinding { get; } + + /// + /// Gets a boolean indicating whether data bindings are supported on this type of property + /// + public bool DataBindingsSupported { get; } + /// /// Gets the unique path of the property on the layer /// @@ -47,11 +61,6 @@ namespace Artemis.Core /// void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); - /// - /// Returns a list off all data binding registrations - /// - List GetAllDataBindingRegistrations(); - /// /// Attempts to load and add the provided keyframe entity to the layer property /// @@ -70,6 +79,12 @@ namespace Artemis.Core /// The timeline to apply to the property void Update(Timeline timeline); + + /// + /// Updates just the data binding instead of the entire layer + /// + void UpdateDataBinding(); + /// /// Occurs when the layer property is disposed /// @@ -104,25 +119,5 @@ namespace Artemis.Core /// Occurs when a keyframe was removed from the layer property /// public event EventHandler? KeyframeRemoved; - - /// - /// Occurs when a data binding property has been added - /// - public event EventHandler? DataBindingPropertyRegistered; - - /// - /// Occurs when all data binding properties have been removed - /// - public event EventHandler? DataBindingPropertiesCleared; - - /// - /// Occurs when a data binding has been enabled - /// - public event EventHandler? DataBindingEnabled; - - /// - /// Occurs when a data binding has been disabled - /// - public event EventHandler? DataBindingDisabled; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 3d83ce37b..d75e47af0 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -30,6 +30,7 @@ namespace Artemis.Core Path = null!; Entity = null!; PropertyDescription = null!; + DataBinding = null!; CurrentValue = default!; DefaultValue = default!; @@ -57,93 +58,10 @@ namespace Artemis.Core { _disposed = true; - foreach (IDataBinding dataBinding in _dataBindings) - dataBinding.Dispose(); - + DataBinding.Dispose(); Disposed?.Invoke(this, EventArgs.Empty); } - /// - /// Invokes the event - /// - protected virtual void OnUpdated() - { - Updated?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnCurrentValueSet() - { - CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); - LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframesToggled() - { - KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframeAdded() - { - KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframeRemoved() - { - KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertyRegistered() - { - DataBindingPropertyRegistered?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertiesCleared() - { - DataBindingPropertiesCleared?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingEnabled(LayerPropertyEventArgs e) - { - DataBindingEnabled?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingDisabled(LayerPropertyEventArgs e) - { - DataBindingDisabled?.Invoke(this, e); - } - /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } @@ -162,7 +80,16 @@ namespace Artemis.Core CurrentValue = BaseValue; UpdateKeyframes(timeline); - UpdateDataBindings(timeline); + UpdateDataBinding(); + + // UpdateDataBinding called OnUpdated() + } + + /// + public void UpdateDataBinding() + { + DataBinding.Update(); + DataBinding.Apply(); OnUpdated(); } @@ -174,39 +101,6 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - /// - public event EventHandler? Disposed; - - /// - public event EventHandler? Updated; - - /// - public event EventHandler? CurrentValueSet; - - /// - public event EventHandler? VisibilityChanged; - - /// - public event EventHandler? KeyframesToggled; - - /// - public event EventHandler? KeyframeAdded; - - /// - public event EventHandler? KeyframeRemoved; - - /// - public event EventHandler? DataBindingPropertyRegistered; - - /// - public event EventHandler? DataBindingPropertiesCleared; - - /// - public event EventHandler? DataBindingEnabled; - - /// - public event EventHandler? DataBindingDisabled; - #region Hierarchy private bool _isHidden; @@ -480,137 +374,19 @@ namespace Artemis.Core #region Data bindings - // ReSharper disable InconsistentNaming - internal readonly List _dataBindingRegistrations = new(); - - internal readonly List _dataBindings = new(); - // ReSharper restore InconsistentNaming - /// - /// Gets whether data bindings are supported on this type of property + /// Gets the data binding of this property /// - public bool DataBindingsSupported { get; protected internal set; } = true; + public DataBinding DataBinding { get; private set; } - /// - /// Gets whether the layer has any active data bindings - /// - public bool HasDataBinding => GetAllDataBindingRegistrations().Any(r => r.GetDataBinding() != null); + /// + public bool DataBindingsSupported => DataBinding.Properties.Any(); - /// - /// Gets a data binding registration by the display name used to register it - /// Note: The expression must exactly match the one used to register the data binding - /// - public DataBindingRegistration? GetDataBindingRegistration(string identifier) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); + /// + public IDataBinding BaseDataBinding => DataBinding; - IDataBindingRegistration? match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration registration && - registration.DisplayName == identifier); - return (DataBindingRegistration?) match; - } - - /// - /// Gets a list containing all data binding registrations of this layer property - /// - /// A list containing all data binding registrations of this layer property - public List GetAllDataBindingRegistrations() - { - return _dataBindingRegistrations; - } - - /// - /// Registers a data binding property so that is available to the data binding system - /// - /// The type of the layer property - /// The function to call to get the value of the property - /// The action to call to set the value of the property - /// The converter to use while applying the data binding - /// The display name of the data binding property - public DataBindingRegistration RegisterDataBindingProperty(Func getter, Action setter, DataBindingConverter converter, - string displayName) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - if (_dataBindingRegistrations.Any(d => d.DisplayName == displayName)) - throw new ArtemisCoreException($"A databinding property named '{displayName}' is already registered."); - - DataBindingRegistration registration = new(this, converter, getter, setter, displayName); - _dataBindingRegistrations.Add(registration); - - // If not yet initialized, load the data binding related to the registration if available - if (_isInitialized) - { - IDataBinding? dataBinding = registration.CreateDataBinding(); - if (dataBinding != null) - _dataBindings.Add(dataBinding); - } - - OnDataBindingPropertyRegistered(); - return registration; - } - - /// - /// Removes all data binding properties so they are no longer available to the data binding system - /// - public void ClearDataBindingProperties() - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) - dataBindingRegistration.ClearDataBinding(); - _dataBindingRegistrations.Clear(); - - OnDataBindingPropertiesCleared(); - } - - /// - /// Enables a data binding for the provided - /// - /// The newly created data binding - public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - if (dataBindingRegistration.LayerProperty != this) - throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); - if (dataBindingRegistration.DataBinding != null) - throw new ArtemisCoreException("Provided data binding registration already has an enabled data binding"); - - DataBinding dataBinding = new(dataBindingRegistration); - _dataBindings.Add(dataBinding); - - OnDataBindingEnabled(new LayerPropertyEventArgs(dataBinding.LayerProperty)); - return dataBinding; - } - - /// - /// Disables the provided data binding - /// - /// The data binding to remove - public void DisableDataBinding(DataBinding dataBinding) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - _dataBindings.Remove(dataBinding); - - if (dataBinding.Registration != null) - dataBinding.Registration.DataBinding = null; - dataBinding.Dispose(); - OnDataBindingDisabled(new LayerPropertyEventArgs(dataBinding.LayerProperty)); - } - - private void UpdateDataBindings(Timeline timeline) - { - foreach (IDataBinding dataBinding in _dataBindings) - { - dataBinding.Update(timeline); - dataBinding.Apply(); - } - } + /// + public bool HasDataBinding => DataBinding.IsEnabled; #endregion @@ -694,6 +470,7 @@ namespace Artemis.Core Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; + DataBinding = new DataBinding(this); if (PropertyDescription.DisableKeyframes) KeyframesSupported = false; @@ -737,13 +514,7 @@ namespace Artemis.Core // ignored for now } - _dataBindings.Clear(); - foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) - { - IDataBinding? dataBinding = dataBindingRegistration.CreateDataBinding(); - if (dataBinding != null) - _dataBindings.Add(dataBinding); - } + DataBinding.Load(); } /// @@ -762,9 +533,8 @@ namespace Artemis.Core Entity.KeyframeEntities.Clear(); Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); - Entity.DataBindingEntities.Clear(); - foreach (IDataBinding dataBinding in _dataBindings) - dataBinding.Save(); + DataBinding.Save(); + Entity.DataBinding = DataBinding.Entity; } /// @@ -775,5 +545,79 @@ namespace Artemis.Core } #endregion + + #region Events + + /// + public event EventHandler? Disposed; + + /// + public event EventHandler? Updated; + + /// + public event EventHandler? CurrentValueSet; + + /// + public event EventHandler? VisibilityChanged; + + /// + public event EventHandler? KeyframesToggled; + + /// + public event EventHandler? KeyframeAdded; + + /// + public event EventHandler? KeyframeRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnUpdated() + { + Updated?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnCurrentValueSet() + { + CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); + LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframesToggled() + { + KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframeAdded() + { + KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframeRemoved() + { + KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index f6a87f5b0..8cef4a100 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -113,11 +113,8 @@ namespace Artemis.Core ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript, Profile) : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); - foreach (ILayerProperty layerProperty in GetAllLayerProperties()) - { - foreach (IDataBindingRegistration dataBindingRegistration in layerProperty.GetAllDataBindingRegistrations()) - dataBindingRegistration.GetDataBinding()?.LoadNodeScript(); - } + foreach (ILayerProperty layerProperty in GetAllLayerProperties()) + layerProperty.BaseDataBinding.LoadNodeScript(); } internal void OnLayerEffectsUpdated() diff --git a/src/Artemis.Core/VisualScripting/DataBindingNodeScript.cs b/src/Artemis.Core/VisualScripting/DataBindingNodeScript.cs new file mode 100644 index 000000000..198fa0cb8 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/DataBindingNodeScript.cs @@ -0,0 +1,36 @@ +using System; +using Artemis.Core.Internal; +using Artemis.Storage.Entities.Profile.Nodes; + +namespace Artemis.Core +{ + public class DataBindingNodeScript : NodeScript + { + #region Properties & Fields + + internal DataBindingExitNode DataBindingExitNode { get; } + + /// + public override Type ResultType => typeof(object); + + #endregion + + /// + public DataBindingNodeScript(string name, string description, DataBinding dataBinding, object? context = null) + : base(name, description, context) + { + DataBindingExitNode = new DataBindingExitNode(dataBinding); + ExitNode = DataBindingExitNode; + AddNode(ExitNode); + } + + /// + public DataBindingNodeScript(string name, string description, DataBinding dataBinding, NodeScriptEntity entity, object? context = null) + : base(name, description, entity, context) + { + DataBindingExitNode = new DataBindingExitNode(dataBinding); + ExitNode = DataBindingExitNode; + AddNode(ExitNode); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/InputPin.cs b/src/Artemis.Core/VisualScripting/InputPin.cs index 0244b6da3..39849a105 100644 --- a/src/Artemis.Core/VisualScripting/InputPin.cs +++ b/src/Artemis.Core/VisualScripting/InputPin.cs @@ -96,7 +96,7 @@ namespace Artemis.Core private void Evaluate() { - Value = ConnectedTo.Count > 0 ? ConnectedTo[0].PinValue : default; + Value = ConnectedTo.Count > 0 ? ConnectedTo[0].PinValue : Type.GetDefault(); } #endregion diff --git a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs new file mode 100644 index 000000000..ef1102d85 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core.Internal +{ + internal class DataBindingExitNode : Node, IExitNode + { + private readonly Dictionary _propertyPins = new(); + private readonly Dictionary _propertyValues = new(); + + public DataBinding DataBinding { get; } + + public DataBindingExitNode(DataBinding dataBinding) : base(dataBinding.LayerProperty.PropertyDescription.Name ?? "", "") + { + DataBinding = dataBinding; + DataBinding.DataBindingPropertiesCleared += DataBindingOnDataBindingPropertiesCleared; + DataBinding.DataBindingPropertyRegistered += DataBindingOnDataBindingPropertyRegistered; + + CreateInputPins(); + } + + public override bool IsExitNode => true; + + public override void Evaluate() + { + foreach (var (property, inputPin) in _propertyPins) + { + if (inputPin.ConnectedTo.Any()) + _propertyValues[property] = inputPin.Value; + else + _propertyValues.Remove(property); + } + } + + public void ApplyToDataBinding() + { + foreach (var (property, pendingValue) in _propertyValues) + property.SetValue(pendingValue); + } + + private void ClearInputPins() + { + while (Pins.Any()) + RemovePin((Pin) Pins.First()); + + _propertyPins.Clear(); + _propertyValues.Clear(); + } + + private void CreateInputPins() + { + ClearInputPins(); + + foreach (IDataBindingProperty property in DataBinding.Properties) + _propertyPins.Add(property, CreateInputPin(property.ValueType, property.DisplayName)); + } + + #region Event handlers + + private void DataBindingOnDataBindingPropertyRegistered(object? sender, DataBindingEventArgs e) + { + CreateInputPins(); + } + + private void DataBindingOnDataBindingPropertiesCleared(object? sender, DataBindingEventArgs e) + { + ClearInputPins(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs index 92a38fb1a..1160c38ae 100644 --- a/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/DataBindings/DataBindingEntity.cs @@ -1,14 +1,10 @@ -using System; -using Artemis.Storage.Entities.Profile.Nodes; +using Artemis.Storage.Entities.Profile.Nodes; namespace Artemis.Storage.Entities.Profile.DataBindings { public class DataBindingEntity { public string Identifier { get; set; } - public TimeSpan EasingTime { get; set; } - public int EasingFunction { get; set; } - public NodeScriptEntity NodeScript { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs index 6c04025d4..ec5fc0344 100644 --- a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs @@ -8,16 +8,15 @@ namespace Artemis.Storage.Entities.Profile public PropertyEntity() { KeyframeEntities = new List(); - DataBindingEntities = new List(); } public string FeatureId { get; set; } public string Path { get; set; } + public DataBindingEntity DataBinding { get; set; } public string Value { get; set; } public bool KeyframesEnabled { get; set; } public List KeyframeEntities { get; set; } - public List DataBindingEntities { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs b/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs index a5e043888..f03156dd0 100644 --- a/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs +++ b/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs @@ -44,6 +44,11 @@ namespace Artemis.UI.Shared /// public LayerProperty LayerProperty { get; } + /// + /// Gets a boolean indicating whether the layer property should be enabled + /// + public bool IsEnabled => !LayerProperty.HasDataBinding; + /// /// Gets the profile editor service /// @@ -85,8 +90,8 @@ namespace Artemis.UI.Shared { LayerProperty.Updated += LayerPropertyOnUpdated; LayerProperty.CurrentValueSet += LayerPropertyOnUpdated; - LayerProperty.DataBindingEnabled += LayerPropertyOnDataBindingChange; - LayerProperty.DataBindingDisabled += LayerPropertyOnDataBindingChange; + LayerProperty.DataBinding.DataBindingEnabled += OnDataBindingChange; + LayerProperty.DataBinding.DataBindingDisabled += OnDataBindingChange; UpdateInputValue(); base.OnInitialActivate(); } @@ -96,8 +101,8 @@ namespace Artemis.UI.Shared { LayerProperty.Updated -= LayerPropertyOnUpdated; LayerProperty.CurrentValueSet -= LayerPropertyOnUpdated; - LayerProperty.DataBindingEnabled -= LayerPropertyOnDataBindingChange; - LayerProperty.DataBindingDisabled -= LayerPropertyOnDataBindingChange; + LayerProperty.DataBinding.DataBindingEnabled -= OnDataBindingChange; + LayerProperty.DataBinding.DataBindingDisabled -= OnDataBindingChange; base.OnClose(); } @@ -194,8 +199,9 @@ namespace Artemis.UI.Shared UpdateInputValue(); } - private void LayerPropertyOnDataBindingChange(object? sender, LayerPropertyEventArgs e) + private void OnDataBindingChange(object? sender, DataBindingEventArgs e) { + NotifyOfPropertyChange(nameof(IsEnabled)); OnDataBindingsChanged(); } diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs index d0fdb6e3e..6f1b604eb 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs @@ -37,9 +37,9 @@ namespace Artemis.UI.Shared.Services RenderProfileElement? SelectedProfileElement { get; } /// - /// Gets the currently selected data binding property + /// Gets the currently selected data binding /// - ILayerProperty? SelectedDataBinding { get; } + IDataBinding? SelectedDataBinding { get; } /// /// Gets or sets the current time @@ -89,10 +89,10 @@ namespace Artemis.UI.Shared.Services void SaveSelectedProfileElement(); /// - /// Changes the selected data binding property + /// Changes the selected data binding /// - /// The data binding property to select - void ChangeSelectedDataBinding(ILayerProperty? layerProperty); + /// The data binding to select + void ChangeSelectedDataBinding(IDataBinding? dataBinding); /// /// Updates the profile preview, forcing UI-elements to re-render @@ -114,7 +114,7 @@ namespace Artemis.UI.Shared.Services /// /// Registers a new property input view model used in the profile editor for the generic type defined in /// - /// Note: Registration will remove itself on plugin disable so you don't have to + /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to /// /// /// @@ -123,7 +123,7 @@ namespace Artemis.UI.Shared.Services /// /// Registers a new property input view model used in the profile editor for the generic type defined in /// - /// Note: Registration will remove itself on plugin disable so you don't have to + /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to /// /// /// diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 37cccd627..b93760af4 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -115,7 +115,7 @@ namespace Artemis.UI.Shared.Services // Trigger selected data binding change if (SelectedDataBinding != null) { - SelectedDataBinding = SelectedProfileElement?.GetAllLayerProperties().FirstOrDefault(p => p.Path == SelectedDataBinding.Path); + SelectedDataBinding = SelectedProfileElement?.GetAllLayerProperties().FirstOrDefault(p => p.Path == SelectedDataBinding.BaseLayerProperty.Path)?.BaseDataBinding; OnSelectedDataBindingChanged(); } @@ -184,7 +184,7 @@ namespace Artemis.UI.Shared.Services public ProfileConfiguration? SelectedProfileConfiguration { get; private set; } public Profile? SelectedProfile => SelectedProfileConfiguration?.Profile; public RenderProfileElement? SelectedProfileElement { get; private set; } - public ILayerProperty? SelectedDataBinding { get; private set; } + public IDataBinding? SelectedDataBinding { get; private set; } public TimeSpan CurrentTime { @@ -303,9 +303,9 @@ namespace Artemis.UI.Shared.Services } } - public void ChangeSelectedDataBinding(ILayerProperty? layerProperty) + public void ChangeSelectedDataBinding(IDataBinding? dataBinding) { - SelectedDataBinding = layerProperty; + SelectedDataBinding = dataBinding; OnSelectedDataBindingChanged(); } diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs index 1a3472a72..806d52084 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using Artemis.Core; +using Artemis.Core; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -8,14 +6,10 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class BoolPropertyInputViewModel : PropertyInputViewModel { - private List _registrations; - public BoolPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService) : base(layerProperty, profileEditorService) { - _registrations = layerProperty.GetAllDataBindingRegistrations(); } - public bool IsEnabled => _registrations.Any(r => r.GetDataBinding() != null); protected override void OnDataBindingsChanged() { diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.xaml index 75b8ae201..09f2f664a 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.xaml @@ -5,7 +5,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:dataTemplateSelectors="clr-namespace:Artemis.UI.DataTemplateSelectors" - xmlns:layerBrush="clr-namespace:Artemis.Core.LayerBrushes;assembly=Artemis.Core" xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index bafcc7f30..bdea29e96 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -13,8 +13,8 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class BrushPropertyInputViewModel : PropertyInputViewModel { - private readonly IPluginManagementService _pluginManagementService; private readonly IDialogService _dialogService; + private readonly IPluginManagementService _pluginManagementService; private BindableCollection _descriptors; public BrushPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPluginManagementService pluginManagementService, @@ -51,13 +51,11 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { layer.ChangeLayerBrush(SelectedDescriptor); if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) - { Execute.PostToUIThread(async () => { await Task.Delay(400); await _dialogService.ShowDialogAt("LayerProperties", new Dictionary {{"layerBrush", layer.LayerBrush}}); }); - } } } diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs index c028b564d..f01f483ec 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs @@ -8,19 +8,9 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class FloatPropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _registration; - public FloatPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(layerProperty, profileEditorService, validator) { - _registration = layerProperty.GetDataBindingRegistration("Value"); - } - - public bool IsEnabled => _registration.DataBinding == null; - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsEnabled)); } } diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.xaml index c2c5d3501..cbb7dc9c5 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.xaml @@ -27,7 +27,7 @@ Min="{Binding Start}" DragStarted="{s:Action InputDragStarted}" DragEnded="{s:Action InputDragEnded}" - IsEnabled="{Binding IsEndEnabled}" /> + IsEnabled="{Binding IsEnabled}" /> \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs index 20195e150..2a1271ad5 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs @@ -9,15 +9,10 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class FloatRangePropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _startRegistration; - private readonly DataBindingRegistration _endRegistration; - public FloatRangePropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(layerProperty, profileEditorService, validator) { - _startRegistration = layerProperty.GetDataBindingRegistration("Start"); - _endRegistration = layerProperty.GetDataBindingRegistration("End"); } public float Start @@ -48,20 +43,12 @@ namespace Artemis.UI.DefaultTypes.PropertyInput } } - public bool IsStartEnabled => _startRegistration.DataBinding == null; - public bool IsEndEnabled => _endRegistration.DataBinding == null; protected override void OnInputValueChanged() { NotifyOfPropertyChange(nameof(Start)); NotifyOfPropertyChange(nameof(End)); } - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsStartEnabled)); - NotifyOfPropertyChange(nameof(IsEndEnabled)); - } } public class FloatRangePropertyInputViewModelValidator : AbstractValidator diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs index f729ef554..f972d5e3f 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs @@ -8,19 +8,9 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class IntPropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _registration; - public IntPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(layerProperty, profileEditorService, validator) { - _registration = layerProperty.GetDataBindingRegistration("Value"); - } - - public bool IsEnabled => _registration.DataBinding == null; - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsEnabled)); } } diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.xaml index da9440ce1..8c38caabf 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.xaml @@ -18,7 +18,7 @@ Min="{Binding LayerProperty.PropertyDescription.MinInputValue}" DragStarted="{s:Action InputDragStarted}" DragEnded="{s:Action InputDragEnded}" - IsEnabled="{Binding IsStartEnabled}" /> + IsEnabled="{Binding IsEnabled}" /> - + IsEnabled="{Binding IsEnabled}" /> \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs index eed3e04ff..0358e383f 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs @@ -9,15 +9,10 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class IntRangePropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _startRegistration; - private readonly DataBindingRegistration _endRegistration; - public IntRangePropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(layerProperty, profileEditorService, validator) { - _startRegistration = layerProperty.GetDataBindingRegistration("Start"); - _endRegistration = layerProperty.GetDataBindingRegistration("End"); } public int Start @@ -48,20 +43,12 @@ namespace Artemis.UI.DefaultTypes.PropertyInput } } - public bool IsStartEnabled => _startRegistration.DataBinding == null; - public bool IsEndEnabled => _endRegistration.DataBinding == null; protected override void OnInputValueChanged() { NotifyOfPropertyChange(nameof(Start)); NotifyOfPropertyChange(nameof(End)); } - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsStartEnabled)); - NotifyOfPropertyChange(nameof(IsEndEnabled)); - } } public class IntRangePropertyInputViewModelValidator : AbstractValidator diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml index 026d4d381..bc2c308f9 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml @@ -17,10 +17,10 @@ + DragEnded="{s:Action InputDragEnded}" /> \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs index 9bc2cc186..f2fb56579 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs @@ -7,18 +7,8 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class SKColorPropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _registration; - public SKColorPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService) : base(layerProperty, profileEditorService) { - _registration = layerProperty.GetDataBindingRegistration("Value"); - } - - public bool IsEnabled => _registration.DataBinding == null; - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsEnabled)); } } } \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.xaml index 06cf966ca..55282ff67 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.xaml @@ -18,7 +18,7 @@ Min="{Binding LayerProperty.PropertyDescription.MinInputValue}" DragStarted="{s:Action InputDragStarted}" DragEnded="{s:Action InputDragEnded}" - IsEnabled="{Binding IsXEnabled}" /> + IsEnabled="{Binding IsEnabled}" /> , + IsEnabled="{Binding IsEnabled}" /> \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs index ac32cbaf3..b4b4e4860 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs @@ -10,14 +10,9 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class SKPointPropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _xRegistration; - private readonly DataBindingRegistration _yRegistration; - public SKPointPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(layerProperty, profileEditorService, validator) { - _xRegistration = layerProperty.GetDataBindingRegistration("X"); - _yRegistration = layerProperty.GetDataBindingRegistration("Y"); } public float X @@ -32,20 +27,12 @@ namespace Artemis.UI.DefaultTypes.PropertyInput set => InputValue = new SKPoint(X, value); } - public bool IsXEnabled => _xRegistration.DataBinding == null; - public bool IsYEnabled => _yRegistration.DataBinding == null; protected override void OnInputValueChanged() { NotifyOfPropertyChange(nameof(X)); NotifyOfPropertyChange(nameof(Y)); } - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsXEnabled)); - NotifyOfPropertyChange(nameof(IsYEnabled)); - } } public class SKPointPropertyInputViewModelValidator : AbstractValidator diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.xaml index a15a2b38f..9fbf36a0c 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.xaml @@ -18,7 +18,7 @@ Min="{Binding LayerProperty.PropertyDescription.MinInputValue}" DragStarted="{s:Action InputDragStarted}" DragEnded="{s:Action InputDragEnded}" - IsEnabled="{Binding IsHeightEnabled}" /> + IsEnabled="{Binding IsEnabled}" /> , + IsEnabled="{Binding IsEnabled}" /> \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs index 48707f5c3..023cc700c 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs @@ -12,14 +12,9 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { public class SKSizePropertyInputViewModel : PropertyInputViewModel { - private readonly DataBindingRegistration _heightRegistration; - private readonly DataBindingRegistration _widthRegistration; - public SKSizePropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(layerProperty, profileEditorService, validator) { - _widthRegistration = layerProperty.GetDataBindingRegistration("Width"); - _heightRegistration = layerProperty.GetDataBindingRegistration("Height"); } // Since SKSize is immutable we need to create properties that replace the SKSize entirely @@ -35,20 +30,12 @@ namespace Artemis.UI.DefaultTypes.PropertyInput set => InputValue = new SKSize(Width, value); } - public bool IsWidthEnabled => _widthRegistration.DataBinding == null; - public bool IsHeightEnabled => _heightRegistration.DataBinding == null; protected override void OnInputValueChanged() { NotifyOfPropertyChange(nameof(Width)); NotifyOfPropertyChange(nameof(Height)); } - - protected override void OnDataBindingsChanged() - { - NotifyOfPropertyChange(nameof(IsWidthEnabled)); - NotifyOfPropertyChange(nameof(IsHeightEnabled)); - } } public class SKSizePropertyInputViewModelValidator : AbstractValidator diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 7d1c85aa3..aaf677e09 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -116,11 +116,6 @@ namespace Artemis.UI.Ninject.Factories NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript); } - public interface IDataBindingsVmFactory - { - IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration); - } - public interface IPropertyVmFactory { ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); diff --git a/src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs b/src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs deleted file mode 100644 index 3ab21713f..000000000 --- a/src/Artemis.UI/Ninject/InstanceProviders/DataBindingsViewModelInstanceProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Reflection; -using Artemis.Core; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; -using Ninject.Extensions.Factory; - -namespace Artemis.UI.Ninject.InstanceProviders -{ - public class DataBindingsViewModelInstanceProvider : StandardInstanceProvider - { - protected override Type GetType(MethodInfo methodInfo, object[] arguments) - { - if (methodInfo.ReturnType != typeof(IDataBindingViewModel)) - return base.GetType(methodInfo, arguments); - - // Find LayerProperty type - Type descriptionPropertyType = arguments[0].GetType(); - while (descriptionPropertyType != null && (!descriptionPropertyType.IsGenericType || descriptionPropertyType.GetGenericTypeDefinition() != typeof(DataBindingRegistration<,>))) - descriptionPropertyType = descriptionPropertyType.BaseType; - if (descriptionPropertyType == null) - return base.GetType(methodInfo, arguments); - - return typeof(DataBindingViewModel<,>).MakeGenericType(descriptionPropertyType.GetGenericArguments()); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/UiModule.cs b/src/Artemis.UI/Ninject/UiModule.cs index 549b170c2..02a66a68f 100644 --- a/src/Artemis.UI/Ninject/UiModule.cs +++ b/src/Artemis.UI/Ninject/UiModule.cs @@ -56,7 +56,6 @@ namespace Artemis.UI.Ninject .BindToFactory(); }); - Kernel.Bind().ToFactory(() => new DataBindingsViewModelInstanceProvider()); Kernel.Bind().ToFactory(() => new LayerPropertyViewModelInstanceProvider()); // Bind profile editor VMs diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml deleted file mode 100644 index be5b8b20e..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - Enable data binding - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Result - - - Other bindings not updating? - - - - - - - - - - - - Input - - - Output - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs deleted file mode 100644 index 2808d7a97..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingView.xaml.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Windows.Controls; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings -{ - /// - /// Interaction logic for DataBindingView.xaml - /// - public partial class DataBindingView : UserControl - { - public DataBindingView() - { - InitializeComponent(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs deleted file mode 100644 index 9b0a82562..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Artemis.Core; -using Artemis.Core.Services; -using Artemis.Storage.Entities.Profile.DataBindings; -using Artemis.UI.Exceptions; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings -{ - public sealed class DataBindingViewModel : Screen, IDataBindingViewModel - { - private readonly ICoreService _coreService; - private readonly IProfileEditorService _profileEditorService; - private int _easingTime; - private bool _isDataBindingEnabled; - private bool _isEasingTimeEnabled; - private DateTime _lastUpdate; - private TimelineEasingViewModel _selectedEasingViewModel; - - private bool _updating; - private bool _updatingTestResult; - - public DataBindingViewModel(DataBindingRegistration registration, - ICoreService coreService, - ISettingsService settingsService, - IProfileEditorService profileEditorService, - IDataModelUIService dataModelUIService, - INodeService nodeService) - { - Registration = registration; - _coreService = coreService; - _profileEditorService = profileEditorService; - - DisplayName = Registration.DisplayName.ToUpper(); - AlwaysApplyDataBindings = settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true); - EasingViewModels = new BindableCollection(); - AvailableNodes = nodeService.AvailableNodes; - TestInputValue = dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), null, true); - TestResultValue = dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), null, true); - } - - public DataBindingRegistration Registration { get; } - public NodeScript Script => Registration.DataBinding?.Script; - - public PluginSetting AlwaysApplyDataBindings { get; } - public BindableCollection EasingViewModels { get; } - public IEnumerable AvailableNodes { get; } - public DataModelDisplayViewModel TestInputValue { get; } - public DataModelDisplayViewModel TestResultValue { get; } - - - public bool IsDataBindingEnabled - { - get => _isDataBindingEnabled; - set - { - SetAndNotify(ref _isDataBindingEnabled, value); - if (_updating) - return; - - if (value) - EnableDataBinding(); - else - DisableDataBinding(); - } - } - - public TimelineEasingViewModel SelectedEasingViewModel - { - get => _selectedEasingViewModel; - set - { - if (!SetAndNotify(ref _selectedEasingViewModel, value)) return; - ApplyChanges(); - } - } - - public int EasingTime - { - get => _easingTime; - set - { - if (!SetAndNotify(ref _easingTime, value)) return; - ApplyChanges(); - } - } - - public bool IsEasingTimeEnabled - { - get => _isEasingTimeEnabled; - set - { - if (!SetAndNotify(ref _isEasingTimeEnabled, value)) return; - ApplyChanges(); - } - } - - public void CopyDataBinding() - { - if (Registration.DataBinding != null) - JsonClipboard.SetObject(Registration.DataBinding.Entity); - } - - public void PasteDataBinding() - { - if (Registration.DataBinding == null) - Registration.LayerProperty.EnableDataBinding(Registration); - if (Registration.DataBinding == null) - throw new ArtemisUIException("Failed to create a data binding in order to paste"); - - DataBindingEntity dataBindingEntity = JsonClipboard.GetData(); - if (dataBindingEntity == null) - return; - - Registration.DataBinding.EasingTime = dataBindingEntity.EasingTime; - Registration.DataBinding.EasingFunction = (Easings.Functions) dataBindingEntity.EasingFunction; - - // TODO - Paste visual script - - Update(); - - _profileEditorService.SaveSelectedProfileElement(); - } - - protected override void OnInitialActivate() - { - Initialize(); - base.OnInitialActivate(); - } - - private void Initialize() - { - EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(v => new TimelineEasingViewModel(v, false))); - - _lastUpdate = DateTime.Now; - _coreService.FrameRendered += OnFrameRendered; - Update(); - } - - private void Update() - { - if (_updating) - return; - - if (Registration.DataBinding == null) - { - IsDataBindingEnabled = false; - IsEasingTimeEnabled = false; - return; - } - - _updating = true; - - EasingTime = (int) Registration.DataBinding.EasingTime.TotalMilliseconds; - SelectedEasingViewModel = EasingViewModels.First(vm => vm.EasingFunction == Registration.DataBinding.EasingFunction); - IsDataBindingEnabled = true; - IsEasingTimeEnabled = EasingTime > 0; - - _updating = false; - } - - private void ApplyChanges() - { - if (_updating) - return; - - if (Registration.DataBinding != null) - { - Registration.DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); - Registration.DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; - } - - _profileEditorService.SaveSelectedProfileElement(); - Update(); - } - - private void UpdateTestResult() - { - if (_updating || _updatingTestResult) - return; - - _updatingTestResult = true; - - if (Registration.DataBinding == null) - { - TestInputValue.UpdateValue(default); - TestResultValue.UpdateValue(default); - _updatingTestResult = false; - return; - } - - // If always update is disabled, do constantly update the data binding as long as the view model is open - // If always update is enabled, this is done for all data bindings in ProfileViewModel - if (!AlwaysApplyDataBindings.Value) - { - Registration.DataBinding.UpdateWithDelta(DateTime.Now - _lastUpdate); - _profileEditorService.UpdateProfilePreview(); - } - - TProperty currentValue = Registration.Converter.ConvertFromObject(Registration.DataBinding.Script.Result ?? default(TProperty)); - TestInputValue.UpdateValue(currentValue); - TestResultValue.UpdateValue(Registration.DataBinding != null ? Registration.DataBinding.GetValue(currentValue) : default); - - _updatingTestResult = false; - } - - private void EnableDataBinding() - { - if (Registration.DataBinding != null) - return; - - Registration.LayerProperty.EnableDataBinding(Registration); - NotifyOfPropertyChange(nameof(Script)); - - _profileEditorService.SaveSelectedProfileElement(); - } - - private void DisableDataBinding() - { - if (Registration.DataBinding == null) - return; - - Registration.LayerProperty.DisableDataBinding(Registration.DataBinding); - NotifyOfPropertyChange(nameof(Script)); - Update(); - - _profileEditorService.SaveSelectedProfileElement(); - } - - private void OnFrameRendered(object sender, FrameRenderedEventArgs e) - { - UpdateTestResult(); - _lastUpdate = DateTime.Now; - } - - /// - public void Dispose() - { - _coreService.FrameRendered -= OnFrameRendered; - } - } - - public interface IDataBindingViewModel : IScreen, IDisposable - { - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml index 164f28796..90da2f349 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsView.xaml @@ -4,20 +4,48 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:s="https://github.com/canton7/Stylet" + xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls;assembly=Artemis.VisualScripting" + xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings" + xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800"> - - - - - - - - - - - - + d:DesignHeight="450" d:DesignWidth="800" + d:DataContext="{d:DesignInstance {x:Type layerProperties:DataBindingsViewModel}}"> + + + + + + + + + + + + + Enable data binding + + + + + + + + + Enable this data binding to use nodes! + + + When you enable data bindings you can no longer use keyframes or normal values for this property. + Instead you'll be using our node scripting system to set a value based on the data model or values of other layers/folders. + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs index aee7265a0..a2b4d0846 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingsViewModel.cs @@ -1,88 +1,68 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Ninject.Factories; +using Artemis.Core.Services; using Artemis.UI.Shared.Services; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings { - public class DataBindingsViewModel : Conductor.Collection.AllActive + public class DataBindingsViewModel : Screen { - private readonly IDataBindingsVmFactory _dataBindingsVmFactory; private readonly IProfileEditorService _profileEditorService; - private ILayerProperty _selectedDataBinding; - private int _selectedItemIndex; - private bool _updating; + private IDataBinding _dataBinding; - public DataBindingsViewModel(IProfileEditorService profileEditorService, IDataBindingsVmFactory dataBindingsVmFactory) + public DataBindingsViewModel(IProfileEditorService profileEditorService, INodeService nodeService) { _profileEditorService = profileEditorService; - _dataBindingsVmFactory = dataBindingsVmFactory; + AvailableNodes = nodeService.AvailableNodes.ToList(); } - public int SelectedItemIndex + public List AvailableNodes { get; } + + public IDataBinding DataBinding { - get => _selectedItemIndex; - set => SetAndNotify(ref _selectedItemIndex, value); + get => _dataBinding; + set => SetAndNotify(ref _dataBinding, value); } - private void CreateDataBindingViewModels() + public bool DataBindingEnabled { - int oldIndex = SelectedItemIndex; - Items.Clear(); - - ILayerProperty layerProperty = _profileEditorService.SelectedDataBinding; - if (layerProperty == null) - return; - - List registrations = layerProperty.GetAllDataBindingRegistrations(); - - // Create a data binding VM for each data bindable property. These VMs will be responsible for retrieving - // and creating the actual data bindings - Items.AddRange(registrations.Select(registration => _dataBindingsVmFactory.DataBindingViewModel(registration))); - - SelectedItemIndex = Items.Count < oldIndex ? 0 : oldIndex; + get => _dataBinding?.IsEnabled ?? false; + set + { + if (_dataBinding != null) + _dataBinding.IsEnabled = value; + } } private void ProfileEditorServiceOnSelectedDataBindingChanged(object sender, EventArgs e) { - CreateDataBindingViewModels(); SubscribeToSelectedDataBinding(); - - SelectedItemIndex = 0; } private void SubscribeToSelectedDataBinding() { - if (_selectedDataBinding != null) + if (DataBinding != null) { - _selectedDataBinding.DataBindingPropertyRegistered -= DataBindingRegistrationsChanged; - _selectedDataBinding.DataBindingPropertiesCleared -= DataBindingRegistrationsChanged; + DataBinding.DataBindingEnabled -= DataBindingOnDataBindingToggled; + DataBinding.DataBindingDisabled -= DataBindingOnDataBindingToggled; } - _selectedDataBinding = _profileEditorService.SelectedDataBinding; - if (_selectedDataBinding != null) + DataBinding = _profileEditorService.SelectedDataBinding; + if (DataBinding != null) { - _selectedDataBinding.DataBindingPropertyRegistered += DataBindingRegistrationsChanged; - _selectedDataBinding.DataBindingPropertiesCleared += DataBindingRegistrationsChanged; + DataBinding.DataBindingEnabled += DataBindingOnDataBindingToggled; + DataBinding.DataBindingDisabled += DataBindingOnDataBindingToggled; + + OnPropertyChanged(nameof(DataBindingEnabled)); } } - private void DataBindingRegistrationsChanged(object sender, LayerPropertyEventArgs e) + private void DataBindingOnDataBindingToggled(object? sender, DataBindingEventArgs e) { - if (_updating) - return; - - _updating = true; - Execute.PostToUIThread(async () => - { - await Task.Delay(200); - CreateDataBindingViewModels(); - _updating = false; - }); + OnPropertyChanged(nameof(DataBindingEnabled)); } #region Overrides of Screen @@ -90,17 +70,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings protected override void OnInitialActivate() { _profileEditorService.SelectedDataBindingChanged += ProfileEditorServiceOnSelectedDataBindingChanged; - CreateDataBindingViewModels(); SubscribeToSelectedDataBinding(); base.OnInitialActivate(); } - protected override void OnActivate() - { - SelectedItemIndex = 0; - base.OnActivate(); - } - protected override void OnClose() { _profileEditorService.SelectedDataBindingChanged -= ProfileEditorServiceOnSelectedDataBindingChanged; diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index 12344aec7..9d0ea97d9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -156,7 +156,6 @@ - diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs index e79e4325b..93a2e4d3b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs @@ -39,10 +39,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree public void ActivateDataBindingViewModel() { - if (_profileEditorService.SelectedDataBinding == LayerProperty) + if (_profileEditorService.SelectedDataBinding == LayerProperty.DataBinding) _profileEditorService.ChangeSelectedDataBinding(null); else - _profileEditorService.ChangeSelectedDataBinding(LayerProperty); + _profileEditorService.ChangeSelectedDataBinding(LayerProperty.DataBinding); } public void ResetToDefault() @@ -92,8 +92,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree { _profileEditorService.SelectedDataBindingChanged += ProfileEditorServiceOnSelectedDataBindingChanged; LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged; - LayerProperty.DataBindingEnabled += LayerPropertyOnDataBindingChange; - LayerProperty.DataBindingDisabled += LayerPropertyOnDataBindingChange; + LayerProperty.DataBinding.DataBindingEnabled += LayerPropertyOnDataBindingChange; + LayerProperty.DataBinding.DataBindingDisabled += LayerPropertyOnDataBindingChange; LayerProperty.KeyframesToggled += LayerPropertyOnKeyframesToggled; LayerPropertyViewModel.IsVisible = !LayerProperty.IsHidden; base.OnInitialActivate(); @@ -104,8 +104,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree { _profileEditorService.SelectedDataBindingChanged -= ProfileEditorServiceOnSelectedDataBindingChanged; LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged; - LayerProperty.DataBindingEnabled -= LayerPropertyOnDataBindingChange; - LayerProperty.DataBindingDisabled -= LayerPropertyOnDataBindingChange; + LayerProperty.DataBinding.DataBindingEnabled -= LayerPropertyOnDataBindingChange; + LayerProperty.DataBinding.DataBindingDisabled -= LayerPropertyOnDataBindingChange; LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframesToggled; base.OnClose(); } @@ -116,7 +116,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree private void ProfileEditorServiceOnSelectedDataBindingChanged(object sender, EventArgs e) { - LayerPropertyViewModel.IsHighlighted = _profileEditorService.SelectedDataBinding == LayerProperty; + LayerPropertyViewModel.IsHighlighted = _profileEditorService.SelectedDataBinding == LayerProperty.DataBinding; } private void LayerPropertyOnVisibilityChanged(object sender, LayerPropertyEventArgs e) @@ -124,7 +124,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree LayerPropertyViewModel.IsVisible = !LayerProperty.IsHidden; } - private void LayerPropertyOnDataBindingChange(object sender, LayerPropertyEventArgs e) + private void LayerPropertyOnDataBindingChange(object sender, DataBindingEventArgs e) { NotifyOfPropertyChange(nameof(HasDataBinding)); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index b0549cdd7..40f94cf4e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -30,7 +30,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization private bool _canSelectEditTool; private BindableCollection _devices; private BindableCollection _highlightedLeds; - private DateTime _lastUpdate; private PanZoomViewModel _panZoomViewModel; private Layer _previousSelectedLayer; private int _previousTool; @@ -138,7 +137,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization ApplyActiveProfile(); - _lastUpdate = DateTime.Now; _coreService.FrameRendered += OnFrameRendered; _rgbService.DeviceAdded += RgbServiceOnDevicesModified; @@ -232,7 +230,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization if (CanSelectEditTool == false && ActiveToolIndex == 1) ActivateToolByIndex(2); } - + #region Buttons private void ActivateToolByIndex(int value) @@ -307,19 +305,32 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization private void OnFrameRendered(object sender, FrameRenderedEventArgs e) { - TimeSpan delta = DateTime.Now - _lastUpdate; - _lastUpdate = DateTime.Now; - - if (SuspendedEditing || !_settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true).Value || _profileEditorService.SelectedProfile == null) + if (SuspendedEditing || _profileEditorService.Playing || _profileEditorService.SelectedProfile == null) return; - foreach (IDataBindingRegistration dataBindingRegistration in _profileEditorService.SelectedProfile.GetAllRenderElements() - .SelectMany(f => f.GetAllLayerProperties(), (f, p) => p) - .SelectMany(p => p.GetAllDataBindingRegistrations())) - dataBindingRegistration.GetDataBinding()?.UpdateWithDelta(delta); - - // TODO: Only update when there are data bindings - if (!_profileEditorService.Playing) + bool hasEnabledDataBindings = false; + + // Always update the currently selected data binding + if (_profileEditorService.SelectedDataBinding != null && _profileEditorService.SelectedDataBinding.IsEnabled) + { + hasEnabledDataBindings = true; + _profileEditorService.SelectedDataBinding.BaseLayerProperty.UpdateDataBinding(); + } + + // Update all other data bindings if the user enabled this + if (_settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", true).Value) + { + foreach (ILayerProperty layerProperty in _profileEditorService.SelectedProfile.GetAllRenderElements().SelectMany(f => f.GetAllLayerProperties(), (_, p) => p)) + { + if (layerProperty.BaseDataBinding != _profileEditorService.SelectedDataBinding && layerProperty.BaseDataBinding.IsEnabled) + { + hasEnabledDataBindings = true; + layerProperty.UpdateDataBinding(); + } + } + } + + if (hasEnabledDataBindings) _profileEditorService.UpdateProfilePreview(); } diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs index 05706be9a..f18667794 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs @@ -61,7 +61,7 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels if (_node.ProfileElement == null) return; - LayerProperties.AddRange(_node.ProfileElement.GetAllLayerProperties().Where(l => l.GetAllDataBindingRegistrations().Any())); + LayerProperties.AddRange(_node.ProfileElement.GetAllLayerProperties().Where(l => l.DataBindingsSupported)); _selectedLayerProperty = _node.LayerProperty; NotifyOfPropertyChange(nameof(SelectedLayerProperty)); } diff --git a/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs b/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs index 64f75d022..e07c0b07c 100644 --- a/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs +++ b/src/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs @@ -26,16 +26,16 @@ namespace Artemis.VisualScripting.Nodes return; } - List list = LayerProperty.GetAllDataBindingRegistrations(); + List list = LayerProperty.BaseDataBinding.Properties.ToList(); int index = 0; foreach (IPin pin in Pins) { OutputPin outputPin = (OutputPin) pin; - IDataBindingRegistration dataBindingRegistration = list[index]; + IDataBindingProperty dataBindingProperty = list[index]; index++; // TODO: Is this really non-nullable? - outputPin.Value = dataBindingRegistration.GetValue(); + outputPin.Value = dataBindingProperty.GetValue(); } } } @@ -108,7 +108,7 @@ namespace Artemis.VisualScripting.Nodes if (LayerProperty == null) return; - foreach (IDataBindingRegistration dataBindingRegistration in LayerProperty.GetAllDataBindingRegistrations()) + foreach (IDataBindingProperty dataBindingRegistration in LayerProperty.BaseDataBinding.Properties) CreateOutputPin(dataBindingRegistration.ValueType, dataBindingRegistration.DisplayName); } From 9d6a61f1e51e18c9fa02b0db138a4f4d63a948da Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 22 Aug 2021 22:31:47 +0200 Subject: [PATCH 023/270] Nodes - Added color nodes --- .../Profile/LayerProperties/ILayerProperty.cs | 5 ++ .../Profile/LayerProperties/LayerProperty.cs | 4 +- .../VisualScripting/NodeScript.cs | 20 ++++--- .../Services/RegistrationService.cs | 1 + src/Artemis.UI/packages.lock.json | 5 +- .../Artemis.VisualScripting.csproj | 1 + .../Styles/VisualScriptCablePresenter.xaml | 56 +++++++++++++++---- .../Nodes/Color/BrightenSKColorNode.cs | 27 +++++++++ .../StaticSKColorValueNodeCustomViewModel.cs | 38 +++++++++++++ .../StaticSKColorValueNodeCustomView.xaml | 16 ++++++ .../Nodes/Color/DarkenSKColorNode.cs | 27 +++++++++ .../Nodes/Color/DesaturateSKColorNode.cs | 29 ++++++++++ .../Nodes/Color/HslSKColorNode.cs | 32 +++++++++++ .../Nodes/Color/InvertSKColorNode.cs | 25 +++++++++ .../Nodes/Color/RotateHueSkColorNode.cs | 27 +++++++++ .../Nodes/Color/SaturateSkColorNode.cs | 29 ++++++++++ .../Nodes/Color/StaticSKColorValueNode.cs | 43 ++++++++++++++ .../Nodes/Color/SumSKColorsNode.cs | 46 +++++++++++++++ .../LayerPropertyNodeCustomViewModel.cs | 6 +- .../packages.lock.json | 17 +++--- 20 files changed, 420 insertions(+), 34 deletions(-) create mode 100644 src/Artemis.VisualScripting/Nodes/Color/BrightenSKColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/CustomViews/StaticSKColorValueNodeCustomView.xaml create mode 100644 src/Artemis.VisualScripting/Nodes/Color/DarkenSKColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/DesaturateSKColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/HslSKColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/InvertSKColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/RotateHueSkColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/SaturateSkColorNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/StaticSKColorValueNode.cs create mode 100644 src/Artemis.VisualScripting/Nodes/Color/SumSKColorsNode.cs diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 9391c1027..52ae56bf1 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -22,6 +22,11 @@ namespace Artemis.Core /// LayerPropertyGroup LayerPropertyGroup { get; } + /// + /// Gets or sets whether the property is hidden in the UI + /// + public bool IsHidden { get; set; } + /// /// Gets the data binding of this property /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index d75e47af0..059707618 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -105,9 +105,7 @@ namespace Artemis.Core private bool _isHidden; - /// - /// Gets or sets whether the property is hidden in the UI - /// + /// public bool IsHidden { get => _isHidden; diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 0094d8daa..faeac5616 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -178,9 +178,13 @@ namespace Artemis.Core Entity.Description = Description; Entity.Nodes.Clear(); - int id = 0; + + // No need to save the exit node if that's all there is + if (Nodes.Count() == 1) + return; - Dictionary nodes = new(); + int id = 0; + foreach (INode node in Nodes) { NodeEntity nodeEntity = new() @@ -208,7 +212,6 @@ namespace Artemis.Core } Entity.Nodes.Add(nodeEntity); - nodes.Add(node, id); id++; } @@ -216,20 +219,21 @@ namespace Artemis.Core Entity.Connections.Clear(); foreach (INode node in Nodes) { - SavePins(nodes, node, -1, node.Pins); + SavePins(node, -1, node.Pins); int pinCollectionId = 0; foreach (IPinCollection pinCollection in node.PinCollections) { - SavePins(nodes, node, pinCollectionId, pinCollection); + SavePins(node, pinCollectionId, pinCollection); pinCollectionId++; } } } - private void SavePins(Dictionary nodes, INode node, int collectionId, IEnumerable pins) + private void SavePins(INode node, int collectionId, IEnumerable pins) { int sourcePinId = 0; + List nodes = Nodes.ToList(); foreach (IPin sourcePin in pins.Where(p => p.Direction == PinDirection.Input)) { foreach (IPin targetPin in sourcePin.ConnectedTo) @@ -249,11 +253,11 @@ namespace Artemis.Core Entity.Connections.Add(new NodeConnectionEntity() { SourceType = sourcePin.Type.Name, - SourceNode = nodes[node], + SourceNode = nodes.IndexOf(node), SourcePinCollectionId = collectionId, SourcePinId = sourcePinId, TargetType = targetPin.Type.Name, - TargetNode = nodes[targetPin.Node], + TargetNode = nodes.IndexOf(targetPin.Node), TargetPinCollectionId = targetPinCollectionId, TargetPinId = targetPinId, }); diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 5adbd456b..820ff6fd5 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -126,6 +126,7 @@ namespace Artemis.UI.Services _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(int), new SKColor(0xFF32CD32)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(double), new SKColor(0xFF1E90FF)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(float), new SKColor(0xFFFF7C00)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFF7630C7)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFC842FF)); foreach (Type nodeType in typeof(SumIntegersNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface)) diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 4a5da0c7b..2a76c380f 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -535,8 +535,8 @@ }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "D25rzdCwh+3L+XyXqpNa+H/yiLJbE3/R3K/XexwHyQjGdzZvSufFW3oqf3En7hhqSIsxsJ8f5NEZ0J5W5wlGBg==", + "resolved": "2.80.3", + "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", "dependencies": { "System.Memory": "4.5.3" } @@ -1484,6 +1484,7 @@ "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "2.0.0", "JetBrains.Annotations": "2021.1.0", + "SkiaSharp": "2.80.3", "Stylet": "1.3.6" } } diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index 85b4c6b8e..987d69f58 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml index 223a2b870..57770438c 100644 --- a/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml +++ b/src/Artemis.VisualScripting/Editor/Styles/VisualScriptCablePresenter.xaml @@ -3,14 +3,16 @@ xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:controls="clr-namespace:Artemis.VisualScripting.Editor.Controls" xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters" - xmlns:collections="clr-namespace:System.Collections;assembly=System.Runtime"> - + xmlns:collections="clr-namespace:System.Collections;assembly=System.Runtime" + xmlns:skiaSharp="clr-namespace:SkiaSharp;assembly=SkiaSharp" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"> + @@ -21,8 +23,8 @@ - @@ -36,26 +38,60 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + - + + + + + + + + Click to edit script + + + + + + + + + + + + Configure how the layer should act when the event(s) trigger + + + + + + + + + + + + + + + + + RESTART + + + + + Stop the current run and restart the timeline + + + + + + + + TOGGLE + + + + + Repeat the timeline until the event fires again + + + + + + + + IGNORE + + + + + Ignore subsequent event fires until the timeline finishes + + + + + + + + COPY + + + + + Play another copy of the timeline on top of the current run + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs new file mode 100644 index 000000000..0c0a97b2c --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions +{ + public class DisplayConditionEventViewModel : Screen + { + } +} diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index 44bbe92a3..abaa2d145 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -25,269 +25,253 @@ - + Display conditions - - Not applied during editing - + + + + + + + + + + + + A condition that constantly checks and is either 'on' or 'off' + + + + + + CONSTANT + + + + + + + A condition that checks whenever one or more events fire and acts in a configurable way + + + + + + EVENTS + + + + - - - - - - - - - - - - - - - - Click to edit script - - - - - - - + - - - - - - - - - - - - - - Configure how the layer should act while the conditions above are met - - - - - - - - - - - - - - - Continue repeating the main segment of the timeline while the condition is met - - - - - - REPEAT - - - - - - - Only play the timeline once when the condition is met - - - - - - ONCE - - - - - - - - - - - Configure how the layer should act when the conditions above are no longer met - - - - - - - - - - - - - - - When conditions are no longer met, finish the the current run of the main timeline - - - - - - FINISH - - - - - - - When conditions are no longer met, skip to the end segment of the timeline - - - - - - SKIP TO END - - - - - - - - - - + + - - - - - - Configure how the layer should act when the event(s) trigger + + + + + + + + + + + + + + + + Click to edit script - - - - - - - - - - - - - - - RESTART - - - - - Stop the current run and restart the timeline - - - - - - - - TOGGLE - - - - - Repeat the timeline until the event fires again - - - - - - - - IGNORE - - - - - Ignore subsequent event fires until the timeline finishes - - - - - - - - COPY - - - - - Play another copy of the timeline on top of the current run - - - - - - + + + + + + + + + + + + + + + + + + + + Configure how the layer should act while the conditions above are met + + + + + + + + + + + + + + + Continue repeating the main segment of the timeline while the condition is met + + + + + + REPEAT + + + + + + + Only play the timeline once when the condition is met + + + + + + ONCE + + + + + + + + + + + Configure how the layer should act when the conditions above are no longer met + + + + + + + + + + + + + + + When conditions are no longer met, finish the the current run of the main timeline + + + + + + FINISH + + + + + + + When conditions are no longer met, skip to the end segment of the timeline + + + + + + SKIP TO END + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 72f4bfb91..640d283cd 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -8,7 +8,7 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionsViewModel : Screen, IProfileEditorPanelViewModel + public class DisplayConditionsViewModel : Conductor.Collection.OneActive, IProfileEditorPanelViewModel { private readonly INodeVmFactory _nodeVmFactory; private readonly IProfileEditorService _profileEditorService; @@ -21,6 +21,11 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions _profileEditorService = profileEditorService; _windowManager = windowManager; _nodeVmFactory = nodeVmFactory; + + Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 1"}); + Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 2"}); + Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 3"}); + Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 4"}); } public bool IsEventCondition diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 0845176f7..53f59c83c 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -1492,6 +1492,7 @@ "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "2.0.0", "JetBrains.Annotations": "2021.1.0", + "MaterialDesignThemes": "4.1.0", "Ninject": "3.3.4", "NoStringEvaluating": "2.2.1", "SkiaSharp": "2.80.3", diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index da0811361..215c601ae 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -62,6 +62,9 @@ $(DefaultXamlRuntime) Designer + + $(DefaultXamlRuntime) + $(DefaultXamlRuntime) diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index 1e75ba72e..3860f1ebb 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -198,8 +198,6 @@ namespace Artemis.VisualScripting.Editor.Controls _canvas.RenderTransform = _canvasViewPortTransform = new TranslateTransform(0, 0); _canvas.MouseLeftButtonDown += OnCanvasMouseLeftButtonDown; _canvas.MouseLeftButtonUp += OnCanvasMouseLeftButtonUp; - _canvas.MouseRightButtonDown += OnCanvasMouseRightButtonDown; - _canvas.MouseRightButtonUp += OnCanvasMouseRightButtonUp; _canvas.PreviewMouseRightButtonDown += OnCanvasPreviewMouseRightButtonDown; _canvas.MouseMove += OnCanvasMouseMove; _canvas.MouseWheel += OnCanvasMouseWheel; @@ -292,6 +290,27 @@ namespace Artemis.VisualScripting.Editor.Controls private void OnCanvasMouseLeftButtonDown(object sender, MouseButtonEventArgs args) { + if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) + { + if (AutoFitScript) + { + args.Handled = true; + return; + } + + _dragCanvas = true; + _dragCanvasStartLocation = args.GetPosition(this); + _dragCanvasStartOffset = _viewportCenter; + + _movedDuringDrag = false; + + _canvas.CaptureMouse(); + + args.Handled = true; + + return; + } + VisualScript.DeselectAllNodes(); _boxSelect = true; @@ -317,29 +336,7 @@ namespace Artemis.VisualScripting.Editor.Controls SelectWithinRectangle(new Rect(Canvas.GetLeft(_selectionBorder), Canvas.GetTop(_selectionBorder), _selectionBorder.Width, _selectionBorder.Height)); args.Handled = _movedDuringDrag; } - } - private void OnCanvasMouseRightButtonDown(object sender, MouseButtonEventArgs args) - { - if (AutoFitScript) - { - args.Handled = true; - return; - } - - _dragCanvas = true; - _dragCanvasStartLocation = args.GetPosition(this); - _dragCanvasStartOffset = _viewportCenter; - - _movedDuringDrag = false; - - _canvas.CaptureMouse(); - - args.Handled = true; - } - - private void OnCanvasMouseRightButtonUp(object sender, MouseButtonEventArgs args) - { if (_dragCanvas) { _dragCanvas = false; @@ -353,7 +350,7 @@ namespace Artemis.VisualScripting.Editor.Controls { if (_dragCanvas) { - if (args.RightButton == MouseButtonState.Pressed) + if (args.LeftButton == MouseButtonState.Pressed && (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))) { Vector newLocation = _dragCanvasStartOffset - (((args.GetPosition(this) - _dragCanvasStartLocation)) * (1.0 / Scale)); CenterAt(newLocation); @@ -541,7 +538,7 @@ namespace Artemis.VisualScripting.Editor.Controls List pins = node.Pins.ToList(); pins.AddRange(node.PinCollections.SelectMany(c => c)); pins = pins.Where(p => p.Type == typeof(object) || p.Type == SourcePin.Pin.Type).OrderBy(p => p.Type != typeof(object)).ToList(); - + IPin preferredPin = SourcePin.Pin.Direction == PinDirection.Input ? pins.FirstOrDefault(p => p.Direction == PinDirection.Output) : pins.FirstOrDefault(p => p.Direction == PinDirection.Input); diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs new file mode 100644 index 000000000..0057dcbea --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Stylet; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels +{ + public class DataModelEventNodeCustomViewModel : CustomNodeViewModel + { + private readonly DataModelEventNode _node; + private BindableCollection _modules; + + public DataModelEventNodeCustomViewModel(DataModelEventNode node, ISettingsService settingsService) : base(node) + { + _node = node; + + ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); + ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + } + + public PluginSetting ShowFullPaths { get; } + public PluginSetting ShowDataModelValues { get; } + public BindableCollection FilterTypes { get; } = new() { typeof(DataModelEvent) }; + + public BindableCollection Modules + { + get => _modules; + set => SetAndNotify(ref _modules, value); + } + + public DataModelPath DataModelPath + { + get => _node.DataModelPath; + set + { + if (ReferenceEquals(_node.DataModelPath, value)) + return; + + _node.DataModelPath?.Dispose(); + _node.DataModelPath = value; + _node.DataModelPath.Save(); + + _node.Storage = _node.DataModelPath.Entity; + } + } + + public override void OnActivate() + { + if (Modules != null) + return; + + Modules = new BindableCollection(); + if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + Modules.Add(scriptProfile.Configuration.Module); + else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + Modules.Add(profileConfiguration.Module); + + _node.PropertyChanged += NodeOnPropertyChanged; + } + + public override void OnDeactivate() + { + _node.PropertyChanged -= NodeOnPropertyChanged; + } + + private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(DataModelNode.DataModelPath)) + OnPropertyChanged(nameof(DataModelPath)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs similarity index 100% rename from src/Artemis.VisualScripting/Nodes/CustomViewModels/DataModelNodeCustomViewModel.cs rename to src/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs diff --git a/src/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.xaml new file mode 100644 index 000000000..52708c600 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.xaml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.xaml similarity index 100% rename from src/Artemis.VisualScripting/Nodes/CustomViews/DataModelNodeCustomView.xaml rename to src/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.xaml diff --git a/src/Artemis.VisualScripting/Nodes/DataModelEventNode.cs b/src/Artemis.VisualScripting/Nodes/DataModelEventNode.cs new file mode 100644 index 000000000..26c981864 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/DataModelEventNode.cs @@ -0,0 +1,51 @@ +using System; +using Artemis.Core; +using Artemis.Storage.Entities.Profile; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes +{ + [Node("Data Model-Event", "Responds to a data model event trigger", "External", OutputType = typeof(bool))] + public class DataModelEventNode : Node, IDisposable + { + private DataModelPath _dataModelPath; + + public DataModelEventNode() : base("Data Model-Event", "Responds to a data model event trigger") + { + Output = CreateOutputPin(); + } + + public OutputPin Output { get; } + public INodeScript Script { get; set; } + + public DataModelPath DataModelPath + { + get => _dataModelPath; + set => SetAndNotify(ref _dataModelPath, value); + } + + public override void Initialize(INodeScript script) + { + Script = script; + + if (Storage is not DataModelPathEntity pathEntity) + return; + + DataModelPath = new DataModelPath(null, pathEntity); + DataModelPath.PathValidated += DataModelPathOnPathValidated; + } + + public override void Evaluate() + { + } + + private void DataModelPathOnPathValidated(object sender, EventArgs e) + { + } + + /// + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs index ee40fea9f..155420fc4 100644 --- a/src/Artemis.VisualScripting/Nodes/DataModelNode.cs +++ b/src/Artemis.VisualScripting/Nodes/DataModelNode.cs @@ -84,7 +84,7 @@ namespace Artemis.VisualScripting.Nodes /// public void Dispose() { - DataModelPath.Dispose(); + DataModelPath?.Dispose(); } #endregion From 8ac1431a2f6ea9ac9ab6f8087adc65dabeab8497 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 12 Sep 2021 22:08:34 +0200 Subject: [PATCH 034/270] Core - Split up layer conditions into static- and event-conditions --- .../Artemis.Core.csproj.DotSettings | 1 + .../Profile/Conditions/EventCondition.cs | 98 ++++++++++ .../Profile/Conditions/EventsCondition.cs | 174 ++++++++++++++++++ .../Models/Profile/Conditions/ICondition.cs | 51 +++++ .../Profile/Conditions/StaticCondition.cs | 101 ++++++++++ .../Models/Profile/RenderProfileElement.cs | 100 +++------- .../Profile/Abstract/RenderElementEntity.cs | 2 +- .../CategoryAdaptionHintEntity.cs | 2 - .../DataModelConditionEventEntity.cs | 15 -- .../DataModelConditionEventPredicateEntity.cs | 6 - ...ataModelConditionGeneralPredicateEntity.cs | 6 - .../DataModelConditionGroupEntity.cs | 15 -- .../DataModelConditionListEntity.cs | 16 -- .../DataModelConditionListPredicateEntity.cs | 6 - .../DataModelConditionPredicateEntity.cs | 18 -- .../Conditions/EventsConditionEntity.cs | 18 ++ .../Profile/Conditions/IConditionEntity.cs | 6 + .../Conditions/StaticConditionEntity.cs | 10 + .../DisplayConditionsViewModel.cs | 2 +- 19 files changed, 491 insertions(+), 156 deletions(-) create mode 100644 src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs create mode 100644 src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs create mode 100644 src/Artemis.Core/Models/Profile/Conditions/ICondition.cs create mode 100644 src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGroupEntity.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs delete mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/EventsConditionEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/Conditions/StaticConditionEntity.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 224b124a9..535046f29 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -44,6 +44,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs new file mode 100644 index 000000000..ad7913b54 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -0,0 +1,98 @@ +using System; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + public class EventCondition : CorePropertyChanged, IDisposable, IStorageModel + { + private readonly string _displayName; + private readonly object? _context; + private DateTime _lastProcessedTrigger; + private DataModelPath _eventPath; + + internal EventCondition(string displayName, object? context) + { + _displayName = displayName; + _context = context; + + Entity = new EventConditionEntity(); + Script = new NodeScript($"Activate {displayName}", $"Whether or not the event should activate the {displayName}", context); + } + + internal EventCondition(EventConditionEntity entity, string displayName, object? context) + { + _displayName = displayName; + _context = context; + + Entity = entity; + Script = null!; + + Load(); + } + + /// + /// Gets the script that drives the event condition + /// + public NodeScript Script { get; private set; } + + /// + /// Gets or sets the path to the event that drives this event condition + /// + public DataModelPath EventPath + { + set => SetAndNotify(ref _eventPath, value); + get => _eventPath; + } + + internal EventConditionEntity Entity { get; } + + internal bool Evaluate() + { + if (EventPath.GetValue() is not DataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger) + return false; + + // TODO: Place dataModelEvent.LastEventArgumentsUntyped; in the start node + Script.Run(); + + _lastProcessedTrigger = dataModelEvent.LastTrigger; + + return Script.Result; + } + + #region IDisposable + + /// + public void Dispose() + { + Script.Dispose(); + EventPath.Dispose(); + } + + #endregion + + internal void LoadNodeScript() + { + Script.Load(); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + EventPath = new DataModelPath(null, Entity.EventPath); + Script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", Entity.Script, _context); + } + + /// + public void Save() + { + EventPath.Save(); + Entity.EventPath = EventPath.Entity; + Script.Save(); + Entity.Script = Script.Entity; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs new file mode 100644 index 000000000..8f8af3ad9 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + public class EventsCondition : CorePropertyChanged, INodeScriptCondition + { + private readonly EventsConditionEntity _entity; + private readonly List _eventsList; + private TimeLineEventOverlapMode _eventOverlapMode; + + public EventsCondition(ProfileElement profileElement) + { + _entity = new EventsConditionEntity(); + _eventsList = new List(); + + ProfileElement = profileElement; + Events = new List(_eventsList); + } + + internal EventsCondition(EventsConditionEntity entity, ProfileElement profileElement) + { + _entity = entity; + _eventsList = new List(); + + ProfileElement = profileElement; + Events = new List(_eventsList); + + Load(); + } + + /// + /// Gets a list of events this condition reacts to + /// + public IReadOnlyCollection Events { get; } + + /// + /// Gets or sets how the condition behaves when events trigger before the timeline finishes + /// + public TimeLineEventOverlapMode EventOverlapMode + { + get => _eventOverlapMode; + set => SetAndNotify(ref _eventOverlapMode, value); + } + + /// + public IConditionEntity Entity => _entity; + + /// + public ProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + /// Adds a new event condition + /// + /// The newly created event condition + public EventCondition AddEventCondition() + { + EventCondition eventCondition = new(ProfileElement.GetType().Name.ToLower(), ProfileElement.Profile); + lock (_eventsList) + { + _eventsList.Add(eventCondition); + } + + return eventCondition; + } + + /// + /// Removes the provided event condition + /// + /// The event condition to remove + public void RemoveEventCondition(EventCondition eventCondition) + { + lock (_eventsList) + { + _eventsList.Remove(eventCondition); + } + } + + /// + public void Update() + { + lock (_eventsList) + { + if (EventOverlapMode == TimeLineEventOverlapMode.Toggle) + { + if (_eventsList.Any(c => c.Evaluate())) + IsMet = !IsMet; + } + else + { + IsMet = _eventsList.Any(c => c.Evaluate()); + } + } + } + + /// + public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline) + { + if (!isMet) + return; + + // Event overlap mode doesn't apply in this case + if (timeline.IsFinished) + { + timeline.JumpToStart(); + return; + } + + // If the timeline was already running, look at the event overlap mode + if (EventOverlapMode == TimeLineEventOverlapMode.Restart) + timeline.JumpToStart(); + else if (EventOverlapMode == TimeLineEventOverlapMode.Copy) + timeline.AddExtraTimeline(); + else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet) + timeline.JumpToStart(); + + // The remaining overlap mode is 'ignore' which requires no further action + } + + /// + public void Dispose() + { + foreach (EventCondition eventCondition in Events) + eventCondition.Dispose(); + } + + #region Storage + + /// + public void Load() + { + EventOverlapMode = (TimeLineEventOverlapMode) _entity.EventOverlapMode; + lock (_eventsList) + { + _eventsList.Clear(); + foreach (EventConditionEntity eventCondition in _entity.Events) + _eventsList.Add(new EventCondition(eventCondition, ProfileElement.GetType().Name.ToLower(), ProfileElement.Profile)); + } + } + + /// + public void Save() + { + _entity.EventOverlapMode = (int) EventOverlapMode; + _entity.Events.Clear(); + lock (_eventsList) + { + foreach (EventCondition eventCondition in _eventsList) + { + eventCondition.Save(); + _entity.Events.Add(eventCondition.Entity); + } + } + } + + /// + public void LoadNodeScript() + { + lock (_eventsList) + { + foreach (EventCondition eventCondition in _eventsList) + eventCondition.LoadNodeScript(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs new file mode 100644 index 000000000..e62d21617 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs @@ -0,0 +1,51 @@ +using System; +using Artemis.Storage.Entities.Profile.Abstract; + +namespace Artemis.Core +{ + /// + /// Represents a condition applied to a + /// + public interface ICondition : IDisposable, IStorageModel + { + /// + /// Gets the entity used to store this condition + /// + public IConditionEntity Entity { get; } + + /// + /// Gets the profile element this condition applies to + /// + public ProfileElement ProfileElement { get; } + + /// + /// Gets a boolean indicating whether the condition is currently met + /// + + bool IsMet { get; } + + /// + /// Updates the condition + /// + void Update(); + + /// + /// Applies the display condition to the provided timeline + /// + /// + /// + /// The timeline to apply the display condition to + void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline); + } + + /// + /// Represents a condition applied to a using a + /// + public interface INodeScriptCondition : ICondition + { + /// + /// Loads the node script this node script condition uses + /// + void LoadNodeScript(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs new file mode 100644 index 000000000..f0a8d9e0b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + public class StaticCondition : CorePropertyChanged, INodeScriptCondition + { + private readonly StaticConditionEntity _entity; + + public StaticCondition(ProfileElement profileElement) + { + _entity = new StaticConditionEntity(); + + ProfileElement = profileElement; + string typeDisplayName = profileElement.GetType().Name.ToLower(); + Script = new NodeScript($"Activate {typeDisplayName}", $"Whether or not this {typeDisplayName} should be active", profileElement.Profile); + } + + internal StaticCondition(StaticConditionEntity entity, ProfileElement profileElement) + { + _entity = entity; + + ProfileElement = profileElement; + Script = null!; + + Load(); + } + + /// + /// Gets the script that drives the static condition + /// + public NodeScript Script { get; private set; } + + /// + public IConditionEntity Entity => _entity; + + /// + public ProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + public void Update() + { + if (!Script.HasNodes) + { + IsMet = true; + return; + } + + Script.Run(); + IsMet = Script.Result; + } + + /// + public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline) + { + if (isMet && !wasMet && timeline.IsFinished) + timeline.JumpToStart(); + else if (!isMet && wasMet && timeline.StopMode == TimelineStopMode.SkipToEnd) + timeline.JumpToEndSegment(); + } + + #region IDisposable + + /// + public void Dispose() + { + Script.Dispose(); + } + + #endregion + + #region Storage + + /// + public void Load() + { + string typeDisplayName = ProfileElement.GetType().Name.ToLower(); + Script = new NodeScript($"Activate {typeDisplayName}", $"Whether or not this {typeDisplayName} should be active", _entity.Script, ProfileElement.Profile); + } + + /// + public void Save() + { + Script.Save(); + _entity.Script = Script.Entity; + } + + /// + public void LoadNodeScript() + { + Script.Load(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 37f5d8f8c..dbe3f785c 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -18,17 +18,13 @@ namespace Artemis.Core { private SKRectI _bounds; private SKPath? _path; - private readonly string _typeDisplayName; internal RenderProfileElement(Profile profile) : base(profile) { - _typeDisplayName = this is Layer ? "layer" : "folder"; - _displayCondition = new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); - Timeline = new Timeline(); ExpandedPropertyGroups = new List(); LayerEffectsList = new List(); - LayerEffects = new(LayerEffectsList); + LayerEffects = new ReadOnlyCollection(LayerEffectsList); LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; @@ -64,7 +60,8 @@ namespace Artemis.Core foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.Dispose(); - DisplayCondition.Dispose(); + if (DisplayCondition is IDisposable disposable) + disposable.Dispose(); base.Dispose(disposing); } @@ -97,11 +94,9 @@ namespace Artemis.Core layerEffect.BaseProperties?.ApplyToEntity(); } - // Conditions + // Condition DisplayCondition?.Save(); - RenderElementEntity.NodeScript = DisplayCondition?.Entity; - // RenderElementEntity.DisplayCondition = DisplayCondition?.Entity; - // DisplayCondition?.Save(); + RenderElementEntity.DisplayCondition = DisplayCondition?.Entity; // Timeline RenderElementEntity.Timeline = Timeline?.Entity; @@ -110,11 +105,10 @@ namespace Artemis.Core internal void LoadNodeScript() { - DisplayCondition = RenderElementEntity.NodeScript != null - ? new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", RenderElementEntity.NodeScript, Profile) - : new NodeScript($"Activate {_typeDisplayName}", $"Whether or not this {_typeDisplayName} should be active", Profile); + if (DisplayCondition is INodeScriptCondition scriptCondition) + scriptCondition.LoadNodeScript(); - foreach (ILayerProperty layerProperty in GetAllLayerProperties()) + foreach (ILayerProperty layerProperty in GetAllLayerProperties()) layerProperty.BaseDataBinding.LoadNodeScript(); } @@ -367,20 +361,19 @@ namespace Artemis.Core protected set => SetAndNotify(ref _displayConditionMet, value); } - private NodeScript _displayCondition; private bool _displayConditionMet; - private bool _toggledOnByEvent = false; - /// - /// Gets or sets the root display condition group + /// Gets the display condition used to determine whether this element is active or not /// - public NodeScript DisplayCondition + public ICondition? DisplayCondition { get => _displayCondition; - set => SetAndNotify(ref _displayCondition, value); + private set => SetAndNotify(ref _displayCondition, value); } + private ICondition? _displayCondition; + /// /// Evaluates the display conditions on this element and applies any required changes to the /// @@ -392,63 +385,30 @@ namespace Artemis.Core return; } - if (!DisplayCondition.HasNodes) + if (DisplayCondition == null) { DisplayConditionMet = true; return; } - if (Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle) - _toggledOnByEvent = false; + DisplayCondition.Update(); + DisplayCondition.ApplyToTimeline(DisplayCondition.IsMet, DisplayConditionMet, Timeline); + DisplayConditionMet = DisplayCondition.IsMet; + } - DisplayCondition.Run(); + /// + /// Replaces the current with the provided or + /// + /// + /// The condition to change the to + public void ChangeDisplayCondition(ICondition? condition) + { + if (condition == DisplayCondition) + return; - // TODO: Handle this nicely, right now when there's only an exit node we assume true - bool conditionMet = DisplayCondition.Nodes.Count() == 1 || DisplayCondition.Result; - if (Parent is RenderProfileElement parent && !parent.DisplayConditionMet) - conditionMet = false; - - // if (!DisplayCondition.ContainsEvents) - { - // Regular conditions reset the timeline whenever their condition is met and was not met before that - if (conditionMet && !DisplayConditionMet && Timeline.IsFinished) - Timeline.JumpToStart(); - // If regular conditions are no longer met, jump to the end segment if stop mode requires it - if (!conditionMet && Timeline.StopMode == TimelineStopMode.SkipToEnd) - Timeline.JumpToEndSegment(); - } - // else if (conditionMet) - // { - // if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) - // { - // _toggledOnByEvent = !_toggledOnByEvent; - // if (_toggledOnByEvent) - // Timeline.JumpToStart(); - // } - // else - // { - // // Event conditions reset if the timeline finished - // if (Timeline.IsFinished) - // { - // Timeline.JumpToStart(); - // } - // // and otherwise apply their overlap mode - // else - // { - // if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart) - // Timeline.JumpToStart(); - // else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy) - // Timeline.AddExtraTimeline(); - // // The third option is ignore which is handled below: - // - // // done - // } - // } - // } - - DisplayConditionMet = Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle - ? _toggledOnByEvent - : conditionMet; + ICondition? old = DisplayCondition; + DisplayCondition = condition; + old?.Dispose(); } #endregion diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index 8dc4c6044..26f1d9c53 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -14,7 +14,7 @@ namespace Artemis.Storage.Entities.Profile.Abstract public List PropertyEntities { get; set; } public List ExpandedPropertyGroups { get; set; } - public DataModelConditionGroupEntity DisplayCondition { get; set; } + public IConditionEntity DisplayCondition { get; set; } public TimelineEntity Timeline { get; set; } public NodeScriptEntity NodeScript { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs index bd5fb6f08..64dcc669a 100644 --- a/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs @@ -8,6 +8,4 @@ public int Skip { get; set; } public int Amount { get; set; } } - - } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs deleted file mode 100644 index 141f5bb08..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Profile.Abstract; - -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public class DataModelConditionEventEntity : DataModelConditionPartEntity - { - public DataModelConditionEventEntity() - { - Children = new List(); - } - - public DataModelPathEntity EventPath { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs deleted file mode 100644 index fee134489..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionEventPredicateEntity.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public class DataModelConditionEventPredicateEntity : DataModelConditionPredicateEntity - { - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs deleted file mode 100644 index 1a0fa93ea..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGeneralPredicateEntity.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public class DataModelConditionGeneralPredicateEntity : DataModelConditionPredicateEntity - { - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGroupEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGroupEntity.cs deleted file mode 100644 index 90f84494e..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionGroupEntity.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Profile.Abstract; - -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public class DataModelConditionGroupEntity : DataModelConditionPartEntity - { - public DataModelConditionGroupEntity() - { - Children = new List(); - } - - public int BooleanOperator { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs deleted file mode 100644 index fd812cc41..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListEntity.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using Artemis.Storage.Entities.Profile.Abstract; - -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public class DataModelConditionListEntity : DataModelConditionPartEntity - { - public DataModelConditionListEntity() - { - Children = new List(); - } - - public DataModelPathEntity ListPath { get; set; } - public int ListOperator { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs deleted file mode 100644 index cfc4b5372..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionListPredicateEntity.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public class DataModelConditionListPredicateEntity : DataModelConditionPredicateEntity - { - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs deleted file mode 100644 index c913a6a3c..000000000 --- a/src/Artemis.Storage/Entities/Profile/Conditions/DataModelConditionPredicateEntity.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Artemis.Storage.Entities.Profile.Abstract; - -namespace Artemis.Storage.Entities.Profile.Conditions -{ - public abstract class DataModelConditionPredicateEntity : DataModelConditionPartEntity - { - public int PredicateType { get; set; } - public DataModelPathEntity LeftPath { get; set; } - public DataModelPathEntity RightPath { get; set; } - - public string OperatorType { get; set; } - public Guid? OperatorPluginGuid { get; set; } - - // Stored as a string to be able to control serialization and deserialization ourselves - public string RightStaticValue { get; set; } - } -} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/EventsConditionEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/EventsConditionEntity.cs new file mode 100644 index 000000000..05404882d --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Conditions/EventsConditionEntity.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Nodes; + +namespace Artemis.Storage.Entities.Profile.Conditions +{ + public class EventsConditionEntity : IConditionEntity + { + public int EventOverlapMode { get; set; } + public List Events { get; set; } = new(); + } + + public class EventConditionEntity + { + public DataModelPathEntity EventPath { get; set; } + public NodeScriptEntity Script { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs new file mode 100644 index 000000000..34d5ca589 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Conditions/IConditionEntity.cs @@ -0,0 +1,6 @@ +namespace Artemis.Storage.Entities.Profile.Abstract +{ + public interface IConditionEntity + { + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/Conditions/StaticConditionEntity.cs b/src/Artemis.Storage/Entities/Profile/Conditions/StaticConditionEntity.cs new file mode 100644 index 000000000..38a938c9a --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/Conditions/StaticConditionEntity.cs @@ -0,0 +1,10 @@ +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Nodes; + +namespace Artemis.Storage.Entities.Profile.Conditions +{ + public class StaticConditionEntity : IConditionEntity + { + public NodeScriptEntity Script { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 640d283cd..4936bf68d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -121,7 +121,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (RenderProfileElement == null) return; - _windowManager.ShowDialog(_nodeVmFactory.NodeScriptWindowViewModel(RenderProfileElement.DisplayCondition)); + // _windowManager.ShowDialog(_nodeVmFactory.NodeScriptWindowViewModel(RenderProfileElement.DisplayCondition)); _profileEditorService.SaveSelectedProfileElement(); } From d657e7844ed1a40202a4d118feae65a4daa6bb69 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 14 Sep 2021 22:17:31 +0200 Subject: [PATCH 035/270] Profiles - Split conditions into different types Profile configurations - Added broken state (not yet shown in UI) --- .../Profile/Conditions/EventCondition.cs | 35 ++- .../Profile/Conditions/EventsCondition.cs | 5 +- .../ProfileConfiguration.cs | 9 +- .../Services/Storage/ProfileService.cs | 8 +- .../VisualScripting/Interfaces/INode.cs | 1 + .../Internal/EventStartNode.cs | 49 ++++ src/Artemis.Core/VisualScripting/Node.cs | 2 + .../Ninject/Factories/IVMFactory.cs | 9 + .../DisplayConditionEventViewModel.cs | 13 - .../DisplayConditionsView.xaml | 224 ++---------------- .../DisplayConditionsViewModel.cs | 159 +++++-------- .../Event/EventConditionView.xaml | 81 +++++++ .../Event/EventConditionViewModel.cs | 83 +++++++ .../EventsConditionView.xaml} | 83 +++---- .../Event/EventsConditionViewModel.cs | 65 +++++ .../Static/StaticConditionView.xaml | 181 ++++++++++++++ .../Static/StaticConditionViewModel.cs | 60 +++++ .../Controls/Wrapper/VisualScriptNode.cs | 2 +- 18 files changed, 694 insertions(+), 375 deletions(-) create mode 100644 src/Artemis.Core/VisualScripting/Internal/EventStartNode.cs delete mode 100644 src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs rename src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/{DisplayConditionEventView.xaml => Event/EventsConditionView.xaml} (68%) create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventsConditionViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Static/StaticConditionView.xaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Static/StaticConditionViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index ad7913b54..9b51e0fcc 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using Artemis.Core.Internal; using Artemis.Storage.Entities.Profile.Conditions; namespace Artemis.Core @@ -8,7 +10,7 @@ namespace Artemis.Core private readonly string _displayName; private readonly object? _context; private DateTime _lastProcessedTrigger; - private DataModelPath _eventPath; + private DataModelPath? _eventPath; internal EventCondition(string displayName, object? context) { @@ -17,6 +19,7 @@ namespace Artemis.Core Entity = new EventConditionEntity(); Script = new NodeScript($"Activate {displayName}", $"Whether or not the event should activate the {displayName}", context); + UpdateEventNode(); } internal EventCondition(EventConditionEntity entity, string displayName, object? context) @@ -38,7 +41,7 @@ namespace Artemis.Core /// /// Gets or sets the path to the event that drives this event condition /// - public DataModelPath EventPath + public DataModelPath? EventPath { set => SetAndNotify(ref _eventPath, value); get => _eventPath; @@ -48,7 +51,7 @@ namespace Artemis.Core internal bool Evaluate() { - if (EventPath.GetValue() is not DataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger) + if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger) return false; // TODO: Place dataModelEvent.LastEventArgumentsUntyped; in the start node @@ -65,7 +68,7 @@ namespace Artemis.Core public void Dispose() { Script.Dispose(); - EventPath.Dispose(); + EventPath?.Dispose(); } #endregion @@ -73,6 +76,25 @@ namespace Artemis.Core internal void LoadNodeScript() { Script.Load(); + UpdateEventNode(); + } + + /// + /// Updates the event node, applying the selected event + /// + public void UpdateEventNode() + { + if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent) + return; + + if (Script.Nodes.FirstOrDefault(n => n is EventStartNode) is EventStartNode existing) + existing.UpdateDataModelEvent(dataModelEvent); + else + { + EventStartNode node = new(); + node.UpdateDataModelEvent(dataModelEvent); + Script.AddNode(node); + } } #region Implementation of IStorageModel @@ -82,13 +104,14 @@ namespace Artemis.Core { EventPath = new DataModelPath(null, Entity.EventPath); Script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", Entity.Script, _context); + UpdateEventNode(); } /// public void Save() { - EventPath.Save(); - Entity.EventPath = EventPath.Entity; + EventPath?.Save(); + Entity.EventPath = EventPath?.Entity; Script.Save(); Entity.Script = Script.Entity; } diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs index 8f8af3ad9..86edae3e0 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventsCondition.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -18,7 +19,7 @@ namespace Artemis.Core _eventsList = new List(); ProfileElement = profileElement; - Events = new List(_eventsList); + Events = new ReadOnlyCollection(_eventsList); } internal EventsCondition(EventsConditionEntity entity, ProfileElement profileElement) @@ -27,7 +28,7 @@ namespace Artemis.Core _eventsList = new List(); ProfileElement = profileElement; - Events = new List(_eventsList); + Events = new ReadOnlyCollection(_eventsList); Load(); } diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index b2ce08edb..20ba2ddc3 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -9,7 +9,7 @@ namespace Artemis.Core /// /// Represents the configuration of a profile, contained in a /// - public class ProfileConfiguration : CorePropertyChanged, IStorageModel, IDisposable + public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable { private ProfileCategory _category; private bool _disposed; @@ -277,6 +277,13 @@ namespace Artemis.Core } #endregion + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => "Profile Configuration"; + + #endregion } /// diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 68db68316..d981c27ff 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -221,8 +221,8 @@ namespace Artemis.Core.Services try { // Make sure the profile is active or inactive according to the parameters above - if (shouldBeActive && profileConfiguration.Profile == null) - ActivateProfile(profileConfiguration); + if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile") + profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "Failed to activate profile"); else if (!shouldBeActive && profileConfiguration.Profile != null) DeactivateProfile(profileConfiguration); @@ -597,9 +597,9 @@ namespace Artemis.Core.Services profile.Save(); - foreach (RenderProfileElement renderProfileElement in profile.GetAllRenderElements()) + foreach (RenderProfileElement renderProfileElement in profile.GetAllRenderElements()) renderProfileElement.Save(); - + _logger.Debug("Adapt profile - Saving " + profile); profile.RedoStack.Clear(); profile.UndoStack.Push(memento); diff --git a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs index 915f5bac1..a5d734f3d 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/INode.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/INode.cs @@ -10,6 +10,7 @@ namespace Artemis.Core string Name { get; } string Description { get; } bool IsExitNode { get; } + bool IsDefaultNode { get; } public double X { get; set; } public double Y { get; set; } diff --git a/src/Artemis.Core/VisualScripting/Internal/EventStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventStartNode.cs new file mode 100644 index 000000000..c6fc57242 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Internal/EventStartNode.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Humanizer; + +namespace Artemis.Core.Internal +{ + internal class EventStartNode : Node + { + private IDataModelEvent? _dataModelEvent; + private readonly Dictionary _propertyPins; + + public EventStartNode() : base("Event Arguments", "Contains the event arguments that triggered the evaluation") + { + _propertyPins = new Dictionary(); + } + + public void UpdateDataModelEvent(IDataModelEvent dataModelEvent) + { + if (_dataModelEvent == dataModelEvent) + return; + + foreach (var (_, outputPin) in _propertyPins) + RemovePin(outputPin); + _propertyPins.Clear(); + + _dataModelEvent = dataModelEvent; + foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + _propertyPins.Add(propertyInfo, CreateOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); + } + + #region Overrides of Node + + /// + public override void Evaluate() + { + if (_dataModelEvent?.LastEventArgumentsUntyped == null) + return; + + foreach (var (propertyInfo, outputPin) in _propertyPins) + { + if (outputPin.ConnectedTo.Any()) + outputPin.Value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 4d1acc1eb..669de437f 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -25,6 +25,7 @@ namespace Artemis.Core } private double _x; + public double X { get => _x; @@ -46,6 +47,7 @@ namespace Artemis.Core } public virtual bool IsExitNode => false; + public virtual bool IsDefaultNode => false; private readonly List _pins = new(); public IReadOnlyCollection Pins => new ReadOnlyCollection(_pins); diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index aaf677e09..bc46e929a 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -3,6 +3,8 @@ using Artemis.Core.Modules; using Artemis.Core.ScriptingProviders; using Artemis.UI.Screens.Header; using Artemis.UI.Screens.Plugins; +using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event; +using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static; using Artemis.UI.Screens.ProfileEditor.LayerProperties; using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects; @@ -92,6 +94,13 @@ namespace Artemis.UI.Ninject.Factories TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups); } + public interface IConditionVmFactory : IVmFactory + { + StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); + EventsConditionViewModel EventsConditionViewModel(EventsCondition eventsCondition); + EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); + } + public interface IPrerequisitesVmFactory : IVmFactory { PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs deleted file mode 100644 index 0c0a97b2c..000000000 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionEventViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Stylet; - -namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions -{ - public class DisplayConditionEventViewModel : Screen - { - } -} diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index abaa2d145..9d5962688 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -37,10 +37,28 @@ + + + + + No condition, the element is always displayed + + + + + + NONE + + + @@ -55,9 +73,9 @@ CONSTANT - @@ -78,200 +96,10 @@ - - - - - - - - - - - - - - - - - - - - - - Click to edit script - - - - - - - - - - - - - - - - - - - - - Configure how the layer should act while the conditions above are met - - - - - - - - - - - - - - - Continue repeating the main segment of the timeline while the condition is met - - - - - - REPEAT - - - - - - - Only play the timeline once when the condition is met - - - - - - ONCE - - - - - - - - - - - Configure how the layer should act when the conditions above are no longer met - - - - - - - - - - - - - - - When conditions are no longer met, finish the the current run of the main timeline - - - - - - FINISH - - - - - - - When conditions are no longer met, skip to the end segment of the timeline - - - - - - SKIP TO END - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 4936bf68d..699ea3340 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -1,6 +1,4 @@ -using System.ComponentModel; -using System.Windows.Input; -using Artemis.Core; +using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -8,84 +6,31 @@ using Stylet; namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions { - public class DisplayConditionsViewModel : Conductor.Collection.OneActive, IProfileEditorPanelViewModel + public class DisplayConditionsViewModel : Conductor, IProfileEditorPanelViewModel { - private readonly INodeVmFactory _nodeVmFactory; + private readonly IConditionVmFactory _conditionVmFactory; private readonly IProfileEditorService _profileEditorService; - private readonly IWindowManager _windowManager; - private bool _isEventCondition; - private RenderProfileElement _renderProfileElement; + private DisplayConditionType _displayConditionType; - public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IWindowManager windowManager, INodeVmFactory nodeVmFactory) + public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IConditionVmFactory conditionVmFactory) { _profileEditorService = profileEditorService; - _windowManager = windowManager; - _nodeVmFactory = nodeVmFactory; - - Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 1"}); - Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 2"}); - Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 3"}); - Items.Add(new DisplayConditionEventViewModel() { DisplayName = "Event 4"}); + _conditionVmFactory = conditionVmFactory; } - public bool IsEventCondition + public DisplayConditionType DisplayConditionType { - get => _isEventCondition; - set => SetAndNotify(ref _isEventCondition, value); - } - - public RenderProfileElement RenderProfileElement - { - get => _renderProfileElement; + get => _displayConditionType; set { - if (!SetAndNotify(ref _renderProfileElement, value)) return; - NotifyOfPropertyChange(nameof(DisplayContinuously)); - NotifyOfPropertyChange(nameof(AlwaysFinishTimeline)); - NotifyOfPropertyChange(nameof(EventOverlapMode)); + if (!SetAndNotify(ref _displayConditionType, value)) return; + ChangeConditionType(); } } - public bool DisplayContinuously - { - get => RenderProfileElement?.Timeline.PlayMode == TimelinePlayMode.Repeat; - set - { - TimelinePlayMode playMode = value ? TimelinePlayMode.Repeat : TimelinePlayMode.Once; - if (RenderProfileElement == null || RenderProfileElement?.Timeline.PlayMode == playMode) return; - RenderProfileElement.Timeline.PlayMode = playMode; - _profileEditorService.SaveSelectedProfileElement(); - } - } - - public bool AlwaysFinishTimeline - { - get => RenderProfileElement?.Timeline.StopMode == TimelineStopMode.Finish; - set - { - TimelineStopMode stopMode = value ? TimelineStopMode.Finish : TimelineStopMode.SkipToEnd; - if (RenderProfileElement == null || RenderProfileElement?.Timeline.StopMode == stopMode) return; - RenderProfileElement.Timeline.StopMode = stopMode; - _profileEditorService.SaveSelectedProfileElement(); - } - } - - public TimeLineEventOverlapMode EventOverlapMode - { - get => RenderProfileElement?.Timeline.EventOverlapMode ?? TimeLineEventOverlapMode.Restart; - set - { - if (RenderProfileElement == null || RenderProfileElement?.Timeline.EventOverlapMode == value) return; - RenderProfileElement.Timeline.EventOverlapMode = value; - _profileEditorService.SaveSelectedProfileElement(); - } - } - - public bool ConditionBehaviourEnabled => RenderProfileElement != null; - protected override void OnInitialActivate() { - _profileEditorService.SelectedProfileElementChanged += SelectedProfileEditorServiceOnSelectedProfileElementChanged; + _profileEditorService.SelectedProfileElementChanged += ProfileEditorServiceOnSelectedProfileElementChanged; Update(_profileEditorService.SelectedProfileElement); base.OnInitialActivate(); @@ -93,55 +38,63 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions protected override void OnClose() { - _profileEditorService.SelectedProfileElementChanged -= SelectedProfileEditorServiceOnSelectedProfileElementChanged; + _profileEditorService.SelectedProfileElementChanged -= ProfileEditorServiceOnSelectedProfileElementChanged; base.OnClose(); } + private void ChangeConditionType() + { + if (_profileEditorService.SelectedProfileElement == null) + return; + + if (DisplayConditionType == DisplayConditionType.Static) + _profileEditorService.SelectedProfileElement.ChangeDisplayCondition(new StaticCondition(_profileEditorService.SelectedProfileElement)); + else if (DisplayConditionType == DisplayConditionType.Events) + _profileEditorService.SelectedProfileElement.ChangeDisplayCondition(new EventsCondition(_profileEditorService.SelectedProfileElement)); + else + _profileEditorService.SelectedProfileElement.ChangeDisplayCondition(null); + + _profileEditorService.SaveSelectedProfileElement(); + Update(_profileEditorService.SelectedProfileElement); + } + private void Update(RenderProfileElement renderProfileElement) { - if (RenderProfileElement != null) - RenderProfileElement.Timeline.PropertyChanged -= TimelineOnPropertyChanged; - - RenderProfileElement = renderProfileElement; - - NotifyOfPropertyChange(nameof(DisplayContinuously)); - NotifyOfPropertyChange(nameof(AlwaysFinishTimeline)); - NotifyOfPropertyChange(nameof(ConditionBehaviourEnabled)); - - if (RenderProfileElement != null) - RenderProfileElement.Timeline.PropertyChanged += TimelineOnPropertyChanged; - } - - #region Event handlers - - public void ScriptGridMouseUp(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton != MouseButton.Left) - return; - if (RenderProfileElement == null) + if (renderProfileElement == null) + { + ActiveItem = null; return; + } - // _windowManager.ShowDialog(_nodeVmFactory.NodeScriptWindowViewModel(RenderProfileElement.DisplayCondition)); - _profileEditorService.SaveSelectedProfileElement(); + if (renderProfileElement.DisplayCondition is StaticCondition staticCondition) + { + ActiveItem = _conditionVmFactory.StaticConditionViewModel(staticCondition); + _displayConditionType = DisplayConditionType.Static; + } + else if (renderProfileElement.DisplayCondition is EventsCondition eventsCondition) + { + ActiveItem = _conditionVmFactory.EventsConditionViewModel(eventsCondition); + _displayConditionType = DisplayConditionType.Events; + } + else + { + ActiveItem = null; + _displayConditionType = DisplayConditionType.None; + } + + NotifyOfPropertyChange(nameof(DisplayConditionType)); } - public void EventTriggerModeSelected() - { - _profileEditorService.SaveSelectedProfileElement(); - } - - private void SelectedProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e) + private void ProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e) { Update(e.RenderProfileElement); } + } - private void TimelineOnPropertyChanged(object sender, PropertyChangedEventArgs e) - { - NotifyOfPropertyChange(nameof(DisplayContinuously)); - NotifyOfPropertyChange(nameof(AlwaysFinishTimeline)); - NotifyOfPropertyChange(nameof(EventOverlapMode)); - } - - #endregion + public enum DisplayConditionType + { + None, + Static, + Events } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml new file mode 100644 index 000000000..1ebd5faf6 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + - + NONE diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml index b27aef128..ca4f75c29 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml @@ -1,19 +1,21 @@ - + + @@ -24,21 +26,39 @@ + + + + - + - + + + @@ -66,19 +86,29 @@ - - - - Click to edit script - - - - - Scripts with nothing connected to their end-node are always true. - - + + + Click to edit script + + + + + + + + Conditional trigger disabled + + + + When enabled, the layer will only trigger if the script evaluates to true + + + @@ -86,7 +116,7 @@ - Configure how the layer should act when the event(s) trigger + @@ -102,63 +132,71 @@ - - - RESTART - + IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Restart}, Converter={StaticResource ComparisonConverter}}"> - Stop the current run and restart the timeline + + + + + + + - - - TOGGLE - + IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Toggle}, Converter={StaticResource ComparisonConverter}}"> - Repeat the timeline until the event fires again + + + + + + + - - - IGNORE - + IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Ignore}, Converter={StaticResource ComparisonConverter}}"> - Ignore subsequent event fires until the timeline finishes + + + + + + + - - - COPY - + IsChecked="{Binding EventOverlapMode, ConverterParameter={x:Static core:TimeLineEventOverlapMode.Copy}, Converter={StaticResource ComparisonConverter}}"> - Play another copy of the timeline on top of the current run + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs index 09f790cb0..ec3219de3 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs @@ -12,9 +12,10 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event { public class EventConditionViewModel : Screen { + private readonly INodeVmFactory _nodeVmFactory; private readonly IProfileEditorService _profileEditorService; private readonly IWindowManager _windowManager; - private readonly INodeVmFactory _nodeVmFactory; + private NodeScript _oldScript; public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, IWindowManager windowManager, INodeVmFactory nodeVmFactory) { @@ -22,6 +23,10 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event _windowManager = windowManager; _nodeVmFactory = nodeVmFactory; EventCondition = eventCondition; + + FilterTypes = new BindableCollection {typeof(IDataModelEvent)}; + if (_profileEditorService.SelectedProfileConfiguration?.Module != null) + Modules = new BindableCollection {_profileEditorService.SelectedProfileConfiguration.Module}; } public EventCondition EventCondition { get; } @@ -39,6 +44,42 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event } } + public bool TriggerConditionally + { + get => EventCondition.Script != null; + set + { + if (EventCondition.Script != null) + { + _oldScript = EventCondition.Script; + EventCondition.Script = null; + } + else + { + if (_oldScript != null) + { + EventCondition.Script = _oldScript; + _oldScript = null; + } + else + { + EventCondition.CreateEmptyNodeScript(); + } + } + } + } + + #region Overrides of Screen + + /// + protected override void OnClose() + { + _oldScript?.Dispose(); + base.OnClose(); + } + + #endregion + public void DataModelPathSelected(object sender, DataModelSelectedEventArgs e) { EventCondition.UpdateEventNode(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Static/StaticConditionView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Static/StaticConditionView.xaml index 21a143354..cd33b385c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Static/StaticConditionView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Static/StaticConditionView.xaml @@ -49,17 +49,10 @@ - - - - Click to edit script - - - - - Scripts with nothing connected to their end-node are always true. - - + + Click to edit script + + diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 2d90b2a02..41c1ad47c 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -124,10 +124,10 @@ namespace Artemis.UI.Services _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(bool), new SKColor(0xFFCD3232)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(string), new SKColor(0xFFFFD700)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(int), new SKColor(0xFF32CD32)); - _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(double), new SKColor(0xFF1E90FF)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(float), new SKColor(0xFFFF7C00)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFFAD3EED)); _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFED3E61)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(Enum), new SKColor(0xFF1E90FF)); foreach (Type nodeType in typeof(SumIntegersNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface)) _nodeService.RegisterNodeType(Constants.CorePlugin, nodeType); diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 53f59c83c..8c353ad06 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - ".NETCoreApp,Version=v5.0": { + "net5.0-windows10.0.17763": { "FluentValidation": { "type": "Direct", "requested": "[10.3.0, )", diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs index cd9950722..3a266d336 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodeCreationBox.cs @@ -114,8 +114,8 @@ namespace Artemis.VisualScripting.Editor.Controls return nameContains; if (SourcePin.Pin.Direction == PinDirection.Input) - return nameContains && (nodeData.OutputType == typeof(object) || nodeData.OutputType == SourcePin.Pin.Type); - return nameContains && (nodeData.InputType == typeof(object) || nodeData.InputType == SourcePin.Pin.Type); + return nodeData.OutputType != null && nameContains && (nodeData.OutputType == typeof(object) || nodeData.OutputType.IsAssignableTo(SourcePin.Pin.Type)); + return nodeData.InputType != null && nameContains && (nodeData.InputType == typeof(object) || nodeData.InputType.IsAssignableFrom(SourcePin.Pin.Type)); } private void ItemsSourceChanged() diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs index f14f4bd04..a823cdac6 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPinPresenter.cs @@ -190,6 +190,7 @@ namespace Artemis.VisualScripting.Editor.Controls } private bool IsTypeCompatible(Type type) => (Pin.Pin.Type == type) + || (Pin.Pin.Type == typeof(Enum) && type.IsEnum) || ((Pin.Pin.Direction == PinDirection.Input) && (Pin.Pin.Type == typeof(object))) || ((Pin.Pin.Direction == PinDirection.Output) && (type == typeof(object))); diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs index 3860f1ebb..e2573ed4e 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptPresenter.cs @@ -537,7 +537,7 @@ namespace Artemis.VisualScripting.Editor.Controls // Connect to the first matching input or output pin List pins = node.Pins.ToList(); pins.AddRange(node.PinCollections.SelectMany(c => c)); - pins = pins.Where(p => p.Type == typeof(object) || p.Type == SourcePin.Pin.Type).OrderBy(p => p.Type != typeof(object)).ToList(); + pins = pins.Where(p => p.Type == typeof(object) || p.Type.IsAssignableFrom(SourcePin.Pin.Type)).OrderBy(p => p.Type != typeof(object)).ToList(); IPin preferredPin = SourcePin.Pin.Direction == PinDirection.Input ? pins.FirstOrDefault(p => p.Direction == PinDirection.Output) diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs new file mode 100644 index 000000000..d03fa5395 --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.ComponentModel; +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.UI.Shared; +using Stylet; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels +{ + public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel + { + private readonly EnumEqualsNode _node; + + public EnumEqualsNodeCustomViewModel(EnumEqualsNode node) : base(node) + { + _node = node; + } + + public Enum Input + { + get => _node.Storage as Enum; + set => _node.Storage = value; + } + + public BindableCollection EnumValues { get; } = new(); + + public override void OnActivate() + { + _node.InputPin.PinConnected += InputPinOnPinConnected; + _node.InputPin.PinDisconnected += InputPinOnPinDisconnected; + _node.PropertyChanged += NodeOnPropertyChanged; + + if (_node.InputPin.Value != null && _node.InputPin.Value.GetType().IsEnum) + EnumValues.AddRange(EnumUtilities.GetAllValuesAndDescriptions(_node.InputPin.Value.GetType())); + base.OnActivate(); + } + + public override void OnDeactivate() + { + _node.InputPin.PinConnected -= InputPinOnPinConnected; + _node.InputPin.PinDisconnected -= InputPinOnPinDisconnected; + _node.PropertyChanged -= NodeOnPropertyChanged; + + base.OnDeactivate(); + } + + private void InputPinOnPinDisconnected(object sender, SingleValueEventArgs e) + { + EnumValues.Clear(); + } + + private void InputPinOnPinConnected(object sender, SingleValueEventArgs e) + { + EnumValues.Clear(); + if (_node.InputPin.Value != null && _node.InputPin.Value.GetType().IsEnum) + EnumValues.AddRange(EnumUtilities.GetAllValuesAndDescriptions(_node.InputPin.Value.GetType())); + } + + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Node.Storage)) + OnPropertyChanged(nameof(Input)); + } + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml b/src/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml new file mode 100644 index 000000000..01c753dcf --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/src/Artemis.VisualScripting/Nodes/EnumEqualsNode.cs b/src/Artemis.VisualScripting/Nodes/EnumEqualsNode.cs new file mode 100644 index 000000000..59df2269e --- /dev/null +++ b/src/Artemis.VisualScripting/Nodes/EnumEqualsNode.cs @@ -0,0 +1,30 @@ +using System; +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes +{ + [Node("Enum Equals", "Determines the equality between an input and a selected enum value", InputType = typeof(Enum), OutputType = typeof(bool))] + public class EnumEqualsNode : Node + { + public EnumEqualsNode() : base("Enum Equals", "Determines the equality between an input and a selected enum value") + { + InputPin = CreateInputPin(); + OutputPin = CreateOutputPin(); + } + + public InputPin InputPin { get; } + public OutputPin OutputPin { get; } + + #region Overrides of Node + + /// + public override void Evaluate() + { + OutputPin.Value = InputPin.Value != null && InputPin.Value.Equals(Storage); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.VisualScripting/packages.lock.json b/src/Artemis.VisualScripting/packages.lock.json index 8f10fb4ab..c63f7f03a 100644 --- a/src/Artemis.VisualScripting/packages.lock.json +++ b/src/Artemis.VisualScripting/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - ".NETCoreApp,Version=v5.0": { + "net5.0-windows7.0": { "JetBrains.Annotations": { "type": "Direct", "requested": "[2021.1.0, )", From 02f4609eae19e60857c894d3af06c09f64cace7a Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 18 Sep 2021 13:38:09 +0200 Subject: [PATCH 038/270] Sidebar - Show message when profile is broken Events - Keep the old event type around for if you change your mind --- .../Models/Profile/RenderProfileElement.cs | 17 +----------- .../Services/Storage/ProfileService.cs | 12 ++++++++- .../DisplayConditionsView.xaml | 17 ++++++++++-- .../DisplayConditionsViewModel.cs | 20 +++++++++++--- .../Event/EventConditionView.xaml | 6 ++--- .../Event/EventConditionViewModel.cs | 1 + .../SidebarProfileConfigurationView.xaml | 26 +++++++++++++++++-- .../Screens/Sidebar/SidebarViewModel.cs | 6 +++++ 8 files changed, 78 insertions(+), 27 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index e33de9fc7..6428c82e4 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -377,7 +377,7 @@ namespace Artemis.Core public ICondition? DisplayCondition { get => _displayCondition; - private set => SetAndNotify(ref _displayCondition, value); + set => SetAndNotify(ref _displayCondition, value); } private ICondition? _displayCondition; @@ -404,21 +404,6 @@ namespace Artemis.Core DisplayConditionMet = DisplayCondition.IsMet; } - /// - /// Replaces the current with the provided or - /// - /// - /// The condition to change the to - public void ChangeDisplayCondition(ICondition? condition) - { - if (condition == DisplayCondition) - return; - - ICondition? old = DisplayCondition; - DisplayCondition = condition; - old?.Dispose(); - } - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index d981c27ff..13674ddfd 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -327,7 +327,17 @@ namespace Artemis.Core.Services if (profileConfiguration.Profile != null) return profileConfiguration.Profile; - ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId); + ProfileEntity profileEntity; + try + { + profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId); + } + catch (Exception e) + { + profileConfiguration.SetBrokenState("Failed to activate profile", e); + throw; + } + if (profileEntity == null) throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}"); diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml index beb277709..36e087d64 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsView.xaml @@ -15,7 +15,6 @@ d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance {x:Type displayConditions:DisplayConditionsViewModel}}"> - @@ -100,6 +99,20 @@ s:View.Model="{Binding ActiveItem}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" - IsTabStop="False"/> + IsTabStop="False" + Visibility="{Binding ActiveItem, Converter={StaticResource NullToVisibilityConverter}}"/> + + + + Display conditions disabled + + + + This element will always display. + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs index 134025bf3..56030d322 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/DisplayConditionsViewModel.cs @@ -11,6 +11,8 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private readonly IConditionVmFactory _conditionVmFactory; private readonly IProfileEditorService _profileEditorService; private DisplayConditionType _displayConditionType; + private StaticCondition _staticCondition; + private EventCondition _eventCondition; public DisplayConditionsViewModel(IProfileEditorService profileEditorService, IConditionVmFactory conditionVmFactory) { @@ -47,12 +49,19 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions if (_profileEditorService.SelectedProfileElement == null) return; + // Keep the old condition around in case the user changes their mind + if (_profileEditorService.SelectedProfileElement.DisplayCondition is StaticCondition staticCondition) + _staticCondition = staticCondition; + else if (_profileEditorService.SelectedProfileElement.DisplayCondition is EventCondition eventCondition) + _eventCondition = eventCondition; + + // If we have the old condition around put it back if (DisplayConditionType == DisplayConditionType.Static) - _profileEditorService.SelectedProfileElement.ChangeDisplayCondition(new StaticCondition(_profileEditorService.SelectedProfileElement)); + _profileEditorService.SelectedProfileElement.DisplayCondition = _staticCondition ?? new StaticCondition(_profileEditorService.SelectedProfileElement); else if (DisplayConditionType == DisplayConditionType.Events) - _profileEditorService.SelectedProfileElement.ChangeDisplayCondition(new EventCondition(_profileEditorService.SelectedProfileElement)); + _profileEditorService.SelectedProfileElement.DisplayCondition = _eventCondition ?? new EventCondition(_profileEditorService.SelectedProfileElement); else - _profileEditorService.SelectedProfileElement.ChangeDisplayCondition(null); + _profileEditorService.SelectedProfileElement.DisplayCondition = null; _profileEditorService.SaveSelectedProfileElement(); Update(_profileEditorService.SelectedProfileElement); @@ -87,6 +96,11 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions private void ProfileEditorServiceOnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e) { + if (_staticCondition != null && e.PreviousRenderProfileElement?.DisplayCondition != _staticCondition) + _staticCondition.Dispose(); + if (_eventCondition != null && e.PreviousRenderProfileElement?.DisplayCondition != _eventCondition) + _eventCondition.Dispose(); + Update(e.RenderProfileElement); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml index ca4f75c29..90c7225da 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionView.xaml @@ -50,7 +50,7 @@ IsChecked="{Binding TriggerConditionally}" Content="Conditional trigger" VerticalAlignment="Center" - ToolTip="When enabled, the layer will only trigger if the script evaluates to true" + ToolTip="When enabled, the element will only trigger if the script evaluates to true" Style="{StaticResource MaterialDesignDarkCheckBox}" /> - + @@ -105,7 +105,7 @@ - When enabled, the layer will only trigger if the script evaluates to true + This element will trigger any time the event fires. diff --git a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs index ec3219de3..bdb5eacfb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/DisplayConditions/Event/EventConditionViewModel.cs @@ -84,6 +84,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event { EventCondition.UpdateEventNode(); DisplayName = EventCondition.EventPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? "Invalid event"; + _profileEditorService.SaveSelectedProfileElement(); } public void ScriptGridMouseUp(object sender, MouseButtonEventArgs e) diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.xaml b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.xaml index 3c54f19fc..4cd8e5f90 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.xaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.xaml @@ -19,6 +19,7 @@ + @@ -183,7 +184,13 @@ - + public class ColorToSolidColorBrushConverter : IValueConverter + { + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + + return new SolidColorBrush(!(value is RGBColor color) + ? new Color(0, 0, 0, 0) + : new Color((byte) color.A, (byte) color.R, (byte) color.G, (byte) color.B)); + } + + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return !(value is SolidColorBrush brush) + ? RGBColor.Transparent + : new RGBColor(brush.Color.A, brush.Color.R, brush.Color.G, brush.Color.B); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs b/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs new file mode 100644 index 000000000..1d1977444 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Converters +{ + + public class EnumToCollectionConverter : MarkupExtension, IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return GetAllValuesAndDescriptions(value.GetType()); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return this; + } + + private static string Description(Enum value) + { + object[] attributes = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false); + if (attributes.Any()) + return (attributes.First() as DescriptionAttribute)?.Description; + + // If no description is found, the least we can do is replace underscores with spaces + TextInfo ti = CultureInfo.CurrentCulture.TextInfo; + return ti.ToTitleCase(ti.ToLower(value.ToString().Replace("_", " "))); + } + + private static IEnumerable> GetAllValuesAndDescriptions(Type t) + { + if (!t.IsEnum) + throw new ArgumentException($"{nameof(t)} must be an enum type"); + + return Enum.GetValues(t).Cast().Select(e => new Tuple(e, Description(e))).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/LedIdToStringConverter.cs b/src/Artemis.UI.Avalonia/Converters/LedIdToStringConverter.cs new file mode 100644 index 000000000..1cfe6b6d5 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Converters/LedIdToStringConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using RGB.NET.Core; + +namespace Artemis.UI.Avalonia.Converters +{ + public class LedIdToStringConverter : IValueConverter + { + #region Implementation of IValueConverter + + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.ToString(); + } + + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (Enum.TryParse(typeof(LedId), value?.ToString(), true, out object parsedLedId)) + return parsedLedId; + return LedId.Unknown1; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/NormalizedPercentageConverter.cs b/src/Artemis.UI.Avalonia/Converters/NormalizedPercentageConverter.cs new file mode 100644 index 000000000..5c03d09d3 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Converters/NormalizedPercentageConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Avalonia.Converters +{ + public class NormalizedPercentageConverter : IValueConverter + { + #region IValueConverter Members + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double number) + return number * 100.0; + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is double number) + return number / 100.0; + + return value; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/UriToFileNameConverter.cs b/src/Artemis.UI.Avalonia/Converters/UriToFileNameConverter.cs new file mode 100644 index 000000000..d8b34e3f3 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Converters/UriToFileNameConverter.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; +using System.IO; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Avalonia.Converters +{ + public class UriToFileNameConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Uri uri && uri.IsFile) + return Path.GetFileName(uri.LocalPath); + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return value; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs b/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs new file mode 100644 index 000000000..1139efa67 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Avalonia.Converters +{ + public class ValuesAdditionConverter : IMultiValueConverter + { + + /// + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return values.Where(v => v is double).Cast().Sum(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Artemis.UI.Avalonia/MainWindow.axaml new file mode 100644 index 000000000..ee8375848 --- /dev/null +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs new file mode 100644 index 000000000..be13f759e --- /dev/null +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs @@ -0,0 +1,23 @@ +using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Avalonia; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Main.Views +{ + public partial class MainWindow : ReactiveWindow + { + public MainWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs new file mode 100644 index 000000000..f393dfecb --- /dev/null +++ b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs @@ -0,0 +1,15 @@ +using Artemis.Core; +using Artemis.UI.Avalonia.Screens.Root.ViewModels; + +namespace Artemis.UI.Avalonia.Ninject.Factories +{ + public interface IVmFactory + { + } + + public interface ISidebarVmFactory : IVmFactory + { + SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); + SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs b/src/Artemis.UI.Avalonia/Ninject/UIModule.cs new file mode 100644 index 000000000..74f829d80 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Ninject/UIModule.cs @@ -0,0 +1,43 @@ +using System; +using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Screens; +using Ninject.Extensions.Conventions; +using Ninject.Modules; + +namespace Artemis.UI.Avalonia.Ninject +{ + public class UIModule : NinjectModule + { + public override void Load() + { + if (Kernel == null) + throw new ArgumentNullException("Kernel shouldn't be null here."); + + + Kernel.Bind(x => + { + x.FromThisAssembly() + .SelectAllClasses() + .InheritedFrom() + .BindToSelf(); + }); + + Kernel.Bind(x => + { + x.FromThisAssembly() + .SelectAllClasses() + .InheritedFrom() + .BindAllBaseClasses(); + }); + + // Bind UI factories + Kernel.Bind(x => + { + x.FromThisAssembly() + .SelectAllInterfaces() + .InheritedFrom() + .BindToFactory(); + }); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Program.cs b/src/Artemis.UI.Avalonia/Program.cs new file mode 100644 index 000000000..6d2382455 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Program.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.ReactiveUI; +using Ninject; +using ReactiveUI; +using Splat; +using Splat.Ninject; + +namespace Artemis.UI.Avalonia +{ + class Program + { + private static StandardKernel _kernel; + + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs new file mode 100644 index 000000000..53783ae7b --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs @@ -0,0 +1,15 @@ +namespace Artemis.UI.Avalonia.Screens.Home.ViewModels +{ + public class HomeViewModel : MainScreenViewModel + { + public HomeViewModel() + { + DisplayName = "Home"; + } + + public void OpenUrl(string url) + { + Core.Utilities.OpenUrl(url); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs new file mode 100644 index 000000000..7ad2e7bcd --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs @@ -0,0 +1,7 @@ +namespace Artemis.UI.Avalonia.Screens +{ + public class MainScreenViewModel : ViewModelBase + { + + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs new file mode 100644 index 000000000..2645dc999 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs @@ -0,0 +1,27 @@ +using System; +using Artemis.Core.Services; +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +{ + public class RootViewModel : ViewModelBase, IScreen, IActivatableViewModel + { + private readonly ICoreService _coreService; + + public RootViewModel(ICoreService coreService, SidebarViewModel sidebarViewModel) + { + SidebarViewModel = sidebarViewModel; + _coreService = coreService; + _coreService.Initialize(); + Console.WriteLine("test"); + } + + public SidebarViewModel SidebarViewModel { get; } + + /// + public ViewModelActivator Activator { get; } = new(); + + /// + public RoutingState Router { get; } = new(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarCategoryViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarCategoryViewModel.cs new file mode 100644 index 000000000..0d975bb74 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarCategoryViewModel.cs @@ -0,0 +1,72 @@ +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +{ + public class SidebarCategoryViewModel : ViewModelBase + { + private readonly IProfileService _profileService; + private readonly ISidebarVmFactory _vmFactory; + private SidebarProfileConfigurationViewModel _selectedProfileConfiguration; + public ProfileCategory ProfileCategory { get; } + + public SidebarCategoryViewModel(ProfileCategory profileCategory, IProfileService profileService, ISidebarVmFactory vmFactory) + { + _profileService = profileService; + _vmFactory = vmFactory; + + ProfileCategory = profileCategory; + + if (ShowItems) + CreateProfileViewModels(); + } + + public ObservableCollection ProfileConfigurations { get; } = new(); + + public SidebarProfileConfigurationViewModel SelectedProfileConfiguration + { + get => _selectedProfileConfiguration; + set => this.RaiseAndSetIfChanged(ref _selectedProfileConfiguration, value); + } + + public bool ShowItems + { + get => !ProfileCategory.IsCollapsed; + set + { + ProfileCategory.IsCollapsed = !value; + if (ProfileCategory.IsCollapsed) + ProfileConfigurations.Clear(); + else + CreateProfileViewModels(); + _profileService.SaveProfileCategory(ProfileCategory); + + this.RaisePropertyChanged(nameof(ShowItems)); + } + } + + public bool IsSuspended + { + get => ProfileCategory.IsSuspended; + set + { + ProfileCategory.IsSuspended = value; + this.RaisePropertyChanged(nameof(IsSuspended)); + _profileService.SaveProfileCategory(ProfileCategory); + } + } + + private void CreateProfileViewModels() + { + ProfileConfigurations.Clear(); + foreach (ProfileConfiguration profileConfiguration in ProfileCategory.ProfileConfigurations.OrderBy(p => p.Order)) + ProfileConfigurations.Add(_vmFactory.SidebarProfileConfigurationViewModel(profileConfiguration)); + + SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(i => i.ProfileConfiguration.IsBeingEdited); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs new file mode 100644 index 000000000..bd5fee751 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs @@ -0,0 +1,31 @@ +using Artemis.Core; +using Artemis.Core.Services; + +namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +{ + public class SidebarProfileConfigurationViewModel : ViewModelBase + { + private readonly IProfileService _profileService; + public ProfileConfiguration ProfileConfiguration { get; } + + public SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration, IProfileService profileService) + { + _profileService = profileService; + ProfileConfiguration = profileConfiguration; + + _profileService.LoadProfileConfigurationIcon(ProfileConfiguration); + } + + public bool IsProfileActive => ProfileConfiguration.Profile != null; + + public bool IsSuspended + { + get => ProfileConfiguration.IsSuspended; + set + { + ProfileConfiguration.IsSuspended = value; + _profileService.SaveProfileCategory(ProfileConfiguration.Category); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs new file mode 100644 index 000000000..5d8d7e11a --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs @@ -0,0 +1,31 @@ +using Material.Icons; +using Ninject; + +namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +{ + public class SidebarScreenViewModel : SidebarScreenViewModel where T : MainScreenViewModel + { + public SidebarScreenViewModel(MaterialIconKind icon, string displayName) : base(icon, displayName) + { + } + + public override MainScreenViewModel CreateInstance(IKernel kernel) + { + return kernel.Get(); + } + } + + public abstract class SidebarScreenViewModel : ViewModelBase + { + protected SidebarScreenViewModel(MaterialIconKind icon, string displayName) + { + Icon = icon; + DisplayName = displayName; + } + + public MaterialIconKind Icon { get; } + public string DisplayName { get; } + + public abstract MainScreenViewModel CreateInstance(IKernel kernel); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs new file mode 100644 index 000000000..80a0340f0 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs @@ -0,0 +1,63 @@ +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Screens.Home.ViewModels; +using Artemis.UI.Avalonia.Screens.Settings.ViewModels; +using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels; +using Artemis.UI.Avalonia.Screens.Workshop.ViewModels; +using Material.Icons; +using Ninject; +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +{ + public class SidebarViewModel : ViewModelBase + { + private readonly IKernel _kernel; + private readonly IProfileService _profileService; + private readonly ISidebarVmFactory _sidebarVmFactory; + private SidebarScreenViewModel _selectedSidebarScreen; + + public SidebarViewModel(IKernel kernel, IProfileService profileService, ISidebarVmFactory sidebarVmFactory) + { + _kernel = kernel; + _profileService = profileService; + _sidebarVmFactory = sidebarVmFactory; + + SidebarScreens = new ObservableCollection + { + new SidebarScreenViewModel(MaterialIconKind.Home, "Home"), + new SidebarScreenViewModel(MaterialIconKind.TestTube, "Workshop"), + new SidebarScreenViewModel(MaterialIconKind.Devices, "Surface Editor"), + new SidebarScreenViewModel(MaterialIconKind.Cog, "Settings") + }; + SelectedSidebarScreen = SidebarScreens.First(); + UpdateProfileCategories(); + } + + public ObservableCollection SidebarScreens { get; } + public ObservableCollection SidebarCategories { get; } = new(); + + public SidebarScreenViewModel SelectedSidebarScreen + { + get => _selectedSidebarScreen; + set => this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); + } + + private void UpdateProfileCategories() + { + SidebarCategories.Clear(); + foreach (ProfileCategory profileCategory in _profileService.ProfileCategories.OrderBy(p => p.Order)) + AddProfileCategoryViewModel(profileCategory); + } + + public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) + { + SidebarCategoryViewModel viewModel = _sidebarVmFactory.SidebarCategoryViewModel(profileCategory); + SidebarCategories.Add(viewModel); + return viewModel; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml new file mode 100644 index 000000000..0d4df056b --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml.cs new file mode 100644 index 000000000..547f39877 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml.cs @@ -0,0 +1,19 @@ +using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Root.Views +{ + public class RootView : ReactiveUserControl + { + public RootView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml new file mode 100644 index 000000000..bd9fc5c88 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml.cs new file mode 100644 index 000000000..521586424 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml.cs @@ -0,0 +1,29 @@ +using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Root.Views +{ + public class SidebarCategoryView : ReactiveUserControl + { + public SidebarCategoryView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Title_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (ViewModel != null) + ViewModel.ShowItems = !ViewModel.ShowItems; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarProfileConfigurationView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarProfileConfigurationView.axaml new file mode 100644 index 000000000..109853a54 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarProfileConfigurationView.axaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarProfileConfigurationView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarProfileConfigurationView.axaml.cs new file mode 100644 index 000000000..38d8920f6 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarProfileConfigurationView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Root.Views +{ + public class SidebarProfileConfigurationView : UserControl + { + public SidebarProfileConfigurationView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarScreenView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarScreenView.axaml new file mode 100644 index 000000000..d59b229ae --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarScreenView.axaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarScreenView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarScreenView.axaml.cs new file mode 100644 index 000000000..43f803c9e --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarScreenView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Root.Views +{ + public partial class SidebarScreenView : UserControl + { + public SidebarScreenView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml new file mode 100644 index 000000000..44e0b35c1 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Artemis 2 + + + + + + + + + + + + + + Button.icon-button icon-button-small + + + ToggleButton.icon-button + + + + + HyperlinkButton.icon-button + + + + + HyperlinkButton.icon-button icon-button-small + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml b/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml new file mode 100644 index 000000000..bfed7dcec --- /dev/null +++ b/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml @@ -0,0 +1,20 @@ + + + + + Test + Test + Test + Test + + + + + + + diff --git a/src/Artemis.UI.Avalonia/ViewLocator.cs b/src/Artemis.UI.Avalonia/ViewLocator.cs new file mode 100644 index 000000000..34e508620 --- /dev/null +++ b/src/Artemis.UI.Avalonia/ViewLocator.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +namespace Artemis.UI.Avalonia +{ + public class ViewLocator : IDataTemplate + { + public bool SupportsRecycling => false; + + public IControl Build(object data) + { + string name = data.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); + Type? type = Type.GetType(name); + + if (type != null) + return (Control) Activator.CreateInstance(type)!; + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object data) + { + return data is ViewModelBase; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/ViewModelBase.cs b/src/Artemis.UI.Avalonia/ViewModelBase.cs new file mode 100644 index 000000000..38fbea65d --- /dev/null +++ b/src/Artemis.UI.Avalonia/ViewModelBase.cs @@ -0,0 +1,15 @@ +using ReactiveUI; + +namespace Artemis.UI.Avalonia +{ + public class ViewModelBase : ReactiveObject + { + private string? _displayName; + + public string? DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } + } +} diff --git a/src/Artemis.UI.Avalonia/packages.lock.json b/src/Artemis.UI.Avalonia/packages.lock.json new file mode 100644 index 000000000..0859e9987 --- /dev/null +++ b/src/Artemis.UI.Avalonia/packages.lock.json @@ -0,0 +1,1682 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Avalonia": { + "type": "Direct", + "requested": "[0.10.7, )", + "resolved": "0.10.7", + "contentHash": "savkS5LlA3bGAXvP9RfPRLRjeGp7XXExNRNhM4BaxCHGsopsWOpRt0nNI6X7FrC9Ous3h+k+gKQu8kc+AZHj8A==", + "dependencies": { + "Avalonia.Remote.Protocol": "0.10.7", + "JetBrains.Annotations": "10.3.0", + "System.ComponentModel.Annotations": "4.5.0", + "System.Memory": "4.5.3", + "System.Reactive": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.6.0", + "System.ValueTuple": "4.5.0" + } + }, + "Avalonia.Desktop": { + "type": "Direct", + "requested": "[0.10.7, )", + "resolved": "0.10.7", + "contentHash": "pSVPwUkGB37mzz1ZCW55y2ddiLYVw2bbHu1tmdwbJq1ZgL6xhyEIwCqFemwt2VR/dr74eN8CFlQuU8cHwFZFPA==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Native": "0.10.7", + "Avalonia.Skia": "0.10.7", + "Avalonia.Win32": "0.10.7", + "Avalonia.X11": "0.10.7" + } + }, + "Avalonia.Diagnostics": { + "type": "Direct", + "requested": "[0.10.7, )", + "resolved": "0.10.7", + "contentHash": "SJHcdLfnIR0ImXxqDpQc5GzVcHWtRFJhPLU+kRDqt6z0JGdTbQviYBeVH/uywKVGzi/LgxCPOcC1gjSrV304Jw==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Controls.DataGrid": "0.10.7", + "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.ReactiveUI": { + "type": "Direct", + "requested": "[0.10.7, )", + "resolved": "0.10.7", + "contentHash": "OjmGmTNforTPS9X2/+WqSbuu2h4hNl+N+6SKV83V3KeMKC4HNq6TqOTPNLF2yx2HOm/498I3kVGVXVL4Xan+cA==", + "dependencies": { + "Avalonia": "0.10.7", + "ReactiveUI": "13.2.10", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Svg.Skia": { + "type": "Direct", + "requested": "[0.10.7.2, )", + "resolved": "0.10.7.2", + "contentHash": "a5zt8IwrLeG+2hQ+Hs7/2zjPH5vujFQvcmOOWikM9iktm6Vjejqj8h1ssZK3n4TfGmIkq2AtbQcTBAC1nqFP6A==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Skia": "0.10.7", + "SkiaSharp": "2.80.2", + "Svg.Skia": "0.5.7.2" + } + }, + "FluentAvaloniaUI": { + "type": "Direct", + "requested": "[1.1.3, )", + "resolved": "1.1.3", + "contentHash": "SKobR7BxnOc2eZ/b+jWXRDDtCK0wqJVTn5zplkgJhaQN3re99RSD0KYtcOqTSnfotzE9SbN4D1mWkorCo18xiQ==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Desktop": "0.10.7", + "Avalonia.Diagnostics": "0.10.7" + } + }, + "Live.Avalonia": { + "type": "Direct", + "requested": "[1.3.1, )", + "resolved": "1.3.1", + "contentHash": "FIzh7k2PWsgIBjS4no51ZzWxYmzTG/RzC0DUO6PzoiKkqyKpvSpOvcRg+41Roz6X8VnYCIVm61R7TFlT4eUFKA==", + "dependencies": { + "Avalonia": "0.10.0" + } + }, + "Material.Icons.Avalonia": { + "type": "Direct", + "requested": "[1.0.2, )", + "resolved": "1.0.2", + "contentHash": "hK0MQm2XyPwjT+DiviDOBjrJVQe6V0u+XTDVbxohkq58hUBlq0XZXmHHZ27jUJU6ZVP9ybu44aXfWycbVjnY2A==", + "dependencies": { + "Avalonia": "0.10.0", + "Material.Icons": "1.0.2" + } + }, + "Splat.Ninject": { + "type": "Direct", + "requested": "[13.1.22, )", + "resolved": "13.1.22", + "contentHash": "wRqjUyT6L7G5v7oT75TzQs7knrtZgYF7pNyHoaeJ9sm0aLlzWwy9Z6faZWZ/3fXXJf3IokRFCSXmQSq18UyW4Q==", + "dependencies": { + "Ninject": "3.3.4", + "Splat": "13.1.22" + } + }, + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.0.2020091801", + "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + }, + "Avalonia.Controls.DataGrid": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "AEQ/4We8qak7A3CAWboPvub7AyFH8SdXyoFGVWiJCHRQPy30/apNL5O/dccYnQ2ANO+yTwZxU0mL8RoAJgp+0Q==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Remote.Protocol": "0.10.7", + "JetBrains.Annotations": "10.3.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.FreeDesktop": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "zMjjAqujGe9TiBdst8iHTIAoR0cprR6bxiGrz2Pw/HqbghoM08vv5fHsqCtJVwexdSopkOkge5jkYsKlbl5pdQ==", + "dependencies": { + "Avalonia": "0.10.7", + "Tmds.DBus": "0.9.0" + } + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "1rrTEtvMhIU4atfd6l1nI8npazX2E65cwZ3J66/7lxXaGc/xgYKbZ/uKFHODfv9TRdxRmvcwxk6azJV/Ej86Vg==", + "dependencies": { + "Avalonia": "0.10.7" + } + }, + "Avalonia.Remote.Protocol": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "w8Le0g2StgD1/32mEPYzP0MhMfDyeHvATHFV3IYA2oQTiPoicjUy3To+PuwAB9T9SSi9DtkLhwazlWZOqQGuAw==" + }, + "Avalonia.Skia": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "dV6WXLk5ziYhnDAwf/fagoVRPTBN4LvFt1BMgikvVKCrkWlmtnQQ/Rav9ESJ13c887XpWMGcERYne3vzeLP6MA==", + "dependencies": { + "Avalonia": "0.10.7", + "HarfBuzzSharp": "2.6.1.7", + "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", + "SkiaSharp": "2.80.2", + "SkiaSharp.NativeAssets.Linux": "2.80.2" + } + }, + "Avalonia.Win32": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "pJvd+kYHyZ8OOvThrZ0BkRfTS9xky92pYJ4Aeu8qukdSpTPV06pp5razXNr6FrkVx3CK/3oB8XzLOJdxpRA/VQ==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", + "System.Drawing.Common": "4.5.0", + "System.Numerics.Vectors": "4.5.0" + } + }, + "Avalonia.X11": { + "type": "Transitive", + "resolved": "0.10.7", + "contentHash": "rvmP/TiEUlCtWio4lJGquW/8+1AI+4MNCWNF4ygPlSj1Z9T38FjFXVPM8Cok2W6U8Y4HTrLo4frN4HTiJAtfdw==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.FreeDesktop": "0.10.7", + "Avalonia.Skia": "0.10.7" + } + }, + "Avalonia.Xaml.Behaviors": { + "type": "Transitive", + "resolved": "0.10.7.1", + "contentHash": "Ef5PCejEA6F25GadMwVVqATeWrZ7XFouerQvbeIpH2FOD8iS8Vg8vEJFGkHLv1N6HHUF2nZUb01csfs6bOy4Jg==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Xaml.Interactions": "0.10.7.1", + "Avalonia.Xaml.Interactivity": "0.10.7.1" + } + }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.7.1", + "contentHash": "ZFgGnjUKbZofMAriGn8xPW/3rt1iww2pUTtOQW8+8YQDtFBjZtq2LaiE0ExX9HgPqgrFM8LYwGcf8SsYZb1pNg==", + "dependencies": { + "Avalonia": "0.10.7", + "Avalonia.Xaml.Interactivity": "0.10.7.1" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.7.1", + "contentHash": "vLYy2lRISft55wwU7gKTXdBfbdkas1u/bznO05IYCFQzwVRo5ktSs1U1fy6ZzrK5z9Lq/e05GM7Q123n4Dfpow==", + "dependencies": { + "Avalonia": "0.10.7" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "DynamicData": { + "type": "Transitive", + "resolved": "7.1.1", + "contentHash": "Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "Fizzler": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "CPxuWF8EPvM0rwAtMTR5G+7EuLoGNXsEfqyx06upN9JyVALZ73KgbGn3SLFwGosifiUAXrvNHtXlUwGGytdECg==" + }, + "HarfBuzzSharp": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7" + } + }, + "HidSharp": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.11.10", + "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "0GLU9lwGVXjUNlr9ZIdAgjqLI2Zm/XFGJFaqJ1T1sU+kwfeMLhm68+rblUrNUP9psRl4i8yM7Ghb4ia4oI2E5g==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "LiteDB": { + "type": "Transitive", + "resolved": "5.0.10", + "contentHash": "x70WuqMDuP75dajqSLvO+AnI/BbwS6da+ukTO7rueV7VoXoQ5CRA9FV4r7cOS4OUr2NS1Up7LDIutjCxQycRvg==" + }, + "Material.Icons": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "4UIT91QbedjNfUYU+R3T60U+InozFtIsP1iUzGbkq/G0f1eDE3tXMWUuLEDO3yCEP2MHrPjAOpokwqk1rnWNGA==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + }, + "McMaster.NETCore.Plugins": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "2.9.6", + "contentHash": "Kmms3TxGQMNb95Cu/3K+0bIcMnV4qf/phZBLAB0HUi65rBPxP4JO3aM2LoAcb+DFS600RQJMZ7ZLyYDTbLwJOQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "3ncA7cV+iXGA1VYwe2UEZXcvWyZSlbexWjM9AvocP7sik5UD93qt9Hq0fMRGk0jFRmvmE4T2g+bGfXiBVZEhLw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "2.9.6", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Metadata": "1.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "/LsTtgcMN6Tu1oo7/WYbRAHL4/ubXC/miEakwTpcZKJKtFo7D0AK95Hw0dbGxul6C8WJu60v6NP2435TDYZM+Q==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Scripting": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "tLgqc76qXHmONUhWhxo7z3TcL/LmGFWIUJm1exbQmVJohuQvJnejUMxmVkdxDfMuMZU1fIyJXPZ6Fkp4FEneAg==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.CSharp": "[3.4.0]", + "Microsoft.CodeAnalysis.Common": "[3.4.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "+b6I3DZL2zvck+B/E/aiOveakj5U2G2BcYODQxcGh2IDbatNU3XXxGT1HumkWB5uIZI2Leu0opBgBpjScmjGMA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Ninject": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", + "dependencies": { + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0" + } + }, + "Ninject.Extensions.ChildKernel": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", + "dependencies": { + "Ninject": "3.3.4" + } + }, + "Ninject.Extensions.Conventions": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", + "dependencies": { + "Ninject.Extensions.Factory": "3.3.2" + } + }, + "Ninject.Extensions.Factory": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", + "dependencies": { + "Castle.Core": "4.2.0", + "Ninject": "3.3.3" + } + }, + "ReactiveUI": { + "type": "Transitive", + "resolved": "13.2.10", + "contentHash": "fOCbEZ+RsO2Jhv6vB8VX+ZEvczYJaC95atcSG7oXohJeL/sEwbbqvv9k+tbj2l4bRSj2j5CQvhwA3HNLaxlCAg==", + "dependencies": { + "DynamicData": "7.1.1", + "Splat": "10.0.1", + "System.Reactive": "5.0.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "ShimSkiaSharp": { + "type": "Transitive", + "resolved": "0.5.7.2", + "contentHash": "+KFniLMpqei3Hj1T586ZY/ZDZXXyl6NFLTXvSZ5LvEi96KIcEgbFCuSsNCVfTmnKQaLCn4Gn+KHjqroCJe2FFA==" + }, + "SkiaSharp": { + "type": "Transitive", + "resolved": "2.80.3", + "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SkiaSharp.HarfBuzz": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7", + "SkiaSharp": "2.80.2" + } + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "dependencies": { + "SkiaSharp": "2.80.2" + } + }, + "Splat": { + "type": "Transitive", + "resolved": "13.1.22", + "contentHash": "0t7eWbu2Ub80kdZjkvXa6r/ZLDAPM/q6WDALkpK4urdGUHl5q2XAmuo5QM0CcIWaEt8AFff8mx0H7qE9PxkG7A==" + }, + "Svg.Custom": { + "type": "Transitive", + "resolved": "0.5.7.2", + "contentHash": "BLb8ViaXEWWsYvbxtGj+ITNO1CpZEszWT8sE5Xa1qkeoniIENog5PEKGTc5mAJ8MhFc4GYO33Vo9duH0KzizNQ==", + "dependencies": { + "Fizzler": "1.2.0", + "System.Drawing.Common": "5.0.0", + "System.Memory": "4.5.3", + "System.ObjectModel": "4.3.0", + "System.ValueTuple": "4.5.0" + } + }, + "Svg.Model": { + "type": "Transitive", + "resolved": "0.5.7.2", + "contentHash": "txH95pxkg75s2LBcONXFCmrpYp5flUC5aF1DaLwTtt33dv9WvOQQlxnasNyb0CN7t0yaXpX8CZoo+SU9u1E/eA==", + "dependencies": { + "ShimSkiaSharp": "0.5.7.2", + "Svg.Custom": "0.5.7.2" + } + }, + "Svg.Skia": { + "type": "Transitive", + "resolved": "0.5.7.2", + "contentHash": "QknpGmwSsns+/05W4efAuPvd77AsimuN5bAGTHGzHQPkPY/htgfXPm2YKywsEIpbXvUiTCfvzaSxSlkYHK8F2g==", + "dependencies": { + "SkiaSharp": "2.80.2", + "SkiaSharp.HarfBuzz": "2.80.2", + "Svg.Custom": "0.5.7.2", + "Svg.Model": "0.5.7.2" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "HxozeSlipUK7dAroTYwIcGwKDeOVpQnJlpVaOkBz7CM4TsE5b/tKlQBZecTjh6FzcSbxndYaxxpsBMz+wMJeyw==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.1.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "+MvhNtcvIbqmhANyKu91jQnvIRVSTiaOiFNfKWwXGHG48YAb4I/TyH8spsySiPYla7gKal5ZnF3teJqZAximyQ==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "Tmds.DBus": { + "type": "Transitive", + "resolved": "0.9.0", + "contentHash": "KcTWL9aKuob9Qo2sOTTKFePs1rKGTwZrcBvMFuGVIVR5RojX3oIFj5UBLYfSGjYgrcImC7LjQI3DdCFwUnhNXw==", + "dependencies": { + "System.Reflection.Emit": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, + "artemis.core": { + "type": "Project", + "dependencies": { + "Artemis.Storage": "1.0.0", + "EmbedIO": "3.4.3", + "HidSharp": "2.1.0", + "Humanizer.Core": "2.11.10", + "LiteDB": "5.0.10", + "McMaster.NETCore.Plugins": "1.4.0", + "Newtonsoft.Json": "13.0.1", + "Ninject": "3.3.4", + "Ninject.Extensions.ChildKernel": "3.3.0", + "Ninject.Extensions.Conventions": "3.3.0", + "Serilog": "2.10.0", + "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.0.0", + "SkiaSharp": "2.80.3", + "System.Buffers": "4.5.1", + "System.IO.FileSystem.AccessControl": "5.0.0", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "5.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "artemis.storage": { + "type": "Project", + "dependencies": { + "LiteDB": "5.0.10", + "Serilog": "2.10.0" + } + }, + "artemis.ui.avalonia.shared": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Avalonia": "0.10.7", + "Avalonia.Svg.Skia": "0.10.7.2", + "Avalonia.Xaml.Behaviors": "0.10.7.1", + "Avalonia.Xaml.Interactions": "0.10.7.1", + "Avalonia.Xaml.Interactivity": "0.10.7.1", + "Material.Icons.Avalonia": "1.0.2" + } + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index 3ed7085b2..23ece4496 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - ".NETCoreApp,Version=v5.0": { + "net5.0-windows7.0": { "Humanizer.Core": { "type": "Direct", "requested": "[2.11.10, )", diff --git a/src/Artemis.sln b/src/Artemis.sln index 7f11b07ae..60dcaf778 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -13,32 +13,64 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Shared", "Artemi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Console", "Artemis.ConsoleUI\Artemis.UI.Console.csproj", "{ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Avalonia", "Artemis.UI.Avalonia\Artemis.UI.Avalonia.csproj", "{035CBB38-7B9E-4375-A39C-E9A5B01F23A5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.Avalonia.Shared", "Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.csproj", "{05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {46B74153-77CF-4489-BDF9-D53FDB1F7ACB}.Debug|Any CPU.ActiveCfg = Debug|x64 {46B74153-77CF-4489-BDF9-D53FDB1F7ACB}.Debug|x64.ActiveCfg = Debug|x64 {46B74153-77CF-4489-BDF9-D53FDB1F7ACB}.Debug|x64.Build.0 = Debug|x64 + {46B74153-77CF-4489-BDF9-D53FDB1F7ACB}.Release|Any CPU.ActiveCfg = Release|x64 {46B74153-77CF-4489-BDF9-D53FDB1F7ACB}.Release|x64.ActiveCfg = Release|x64 {46B74153-77CF-4489-BDF9-D53FDB1F7ACB}.Release|x64.Build.0 = Release|x64 + {E489E5E3-1A65-4AF5-A1EA-F9805FD19A65}.Debug|Any CPU.ActiveCfg = Debug|x64 {E489E5E3-1A65-4AF5-A1EA-F9805FD19A65}.Debug|x64.ActiveCfg = Debug|x64 {E489E5E3-1A65-4AF5-A1EA-F9805FD19A65}.Debug|x64.Build.0 = Debug|x64 + {E489E5E3-1A65-4AF5-A1EA-F9805FD19A65}.Release|Any CPU.ActiveCfg = Release|x64 {E489E5E3-1A65-4AF5-A1EA-F9805FD19A65}.Release|x64.ActiveCfg = Release|x64 {E489E5E3-1A65-4AF5-A1EA-F9805FD19A65}.Release|x64.Build.0 = Release|x64 + {9B811F9B-86B9-4771-87AF-72BAE7078A36}.Debug|Any CPU.ActiveCfg = Debug|x64 {9B811F9B-86B9-4771-87AF-72BAE7078A36}.Debug|x64.ActiveCfg = Debug|x64 {9B811F9B-86B9-4771-87AF-72BAE7078A36}.Debug|x64.Build.0 = Debug|x64 + {9B811F9B-86B9-4771-87AF-72BAE7078A36}.Release|Any CPU.ActiveCfg = Release|x64 {9B811F9B-86B9-4771-87AF-72BAE7078A36}.Release|x64.ActiveCfg = Release|x64 {9B811F9B-86B9-4771-87AF-72BAE7078A36}.Release|x64.Build.0 = Release|x64 + {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Debug|Any CPU.ActiveCfg = Debug|x64 {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Debug|x64.ActiveCfg = Debug|x64 {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Debug|x64.Build.0 = Debug|x64 + {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Release|Any CPU.ActiveCfg = Release|x64 {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Release|x64.ActiveCfg = Release|x64 {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Release|x64.Build.0 = Release|x64 + {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Debug|Any CPU.ActiveCfg = Debug|x64 {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Debug|x64.ActiveCfg = Debug|x64 {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Debug|x64.Build.0 = Debug|x64 + {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Release|Any CPU.ActiveCfg = Release|x64 {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Release|x64.ActiveCfg = Release|x64 {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Release|x64.Build.0 = Release|x64 + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|x64.ActiveCfg = Debug|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|x64.Build.0 = Debug|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Release|Any CPU.Build.0 = Release|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Release|x64.ActiveCfg = Release|Any CPU + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Release|x64.Build.0 = Release|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Debug|x64.ActiveCfg = Debug|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Debug|x64.Build.0 = Debug|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|Any CPU.Build.0 = Release|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|x64.ActiveCfg = Release|Any CPU + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 5d8a541624852545aa64471137c7057867bda586 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 10 Oct 2021 22:37:37 +0200 Subject: [PATCH 049/270] UI - Further sidebar improvements and fixed titlebar --- .../Artemis.UI.Avalonia.Shared.csproj | 3 + .../Controls/DeviceVisualizer.cs | 179 +++++++++++++++--- .../Controls/DeviceVisualizerLed.cs | 159 ++++++++++++++++ .../Events/DataModelInputDynamicEventArgs.cs | 21 ++ .../Events/DataModelInputStaticEventArgs.cs | 20 ++ .../Events/LedClickedEventArgs.cs | 27 +++ .../Events/ProfileConfigurationEventArgs.cs | 32 ++++ .../Events/RenderProfileElementEventArgs.cs | 32 ++++ src/Artemis.UI.Avalonia/App.axaml | 9 +- src/Artemis.UI.Avalonia/App.axaml.cs | 5 +- src/Artemis.UI.Avalonia/MainWindow.axaml | 4 +- src/Artemis.UI.Avalonia/MainWindow.axaml.cs | 2 +- src/Artemis.UI.Avalonia/Ninject/UIModule.cs | 2 + .../Screens/Home/ViewModels/HomeViewModel.cs | 6 +- .../Screens/MainScreenViewModel.cs | 18 +- .../Screens/Root/ViewModels/RootViewModel.cs | 9 +- .../Root/ViewModels/SidebarScreenViewModel.cs | 10 +- .../Root/ViewModels/SidebarViewModel.cs | 48 ++++- .../Screens/Root/Views/RootView.axaml | 19 +- .../Screens/Root/Views/SidebarView.axaml | 55 +++--- .../ViewModels/SurfaceEditorViewModel.cs | 6 +- .../ViewModels/SurfaceEditorViewModel.cs | 6 +- .../Workshop/ViewModels/WorkshopViewModel.cs | 6 +- 23 files changed, 583 insertions(+), 95 deletions(-) create mode 100644 src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj index 2ddd5386b..a52cde147 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj @@ -21,5 +21,8 @@ ..\..\..\..\Users\Robert\.nuget\packages\material.icons.avalonia\1.0.2\lib\netstandard2.0\Material.Icons.Avalonia.dll + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs index 841069aed..76c83efc8 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Artemis.Core; +using Artemis.UI.Avalonia.Shared.Events; using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Threading; namespace Artemis.UI.Avalonia.Shared.Controls @@ -18,6 +21,121 @@ namespace Artemis.UI.Avalonia.Shared.Controls /// public class DeviceVisualizer : Control { + private readonly DispatcherTimer _timer; + private readonly List _deviceVisualizerLeds; + + private Bitmap? _deviceImage; + private List? _dimmedLeds; + private List? _highlightedLeds; + private ArtemisDevice? _oldDevice; + + /// + public DeviceVisualizer() + { + // Run an update timer at 25 fps + _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)}; + _deviceVisualizerLeds = new List(); + + PointerReleased += OnPointerReleased; + } + + /// + public override void Render(DrawingContext drawingContext) + { + if (Device == null) + return; + + // Determine the scale required to fit the desired size of the control + Rect measureSize = MeasureDevice(); + double scale = Math.Min(Bounds.Width / measureSize.Width, Bounds.Height / measureSize.Height); + + // Scale the visualization in the desired bounding box + if (Bounds.Width > 0 && Bounds.Height > 0) + drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale)); + + // Apply device rotation + drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - measureSize.Left, 0 - measureSize.Top)); + drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation)); + + // Apply device scale + drawingContext.PushPostTransform(Matrix.CreateScale(Device.Scale, Device.Scale)); + + // Render device and LED images + if (_deviceImage != null) + drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height)); + + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.RenderImage(drawingContext); + } + + /// + /// Occurs when a LED of the device has been clicked + /// + public event EventHandler? LedClicked; + + /// + /// Invokes the event + /// + /// + protected virtual void OnLedClicked(LedClickedEventArgs e) + { + LedClicked?.Invoke(this, e); + } + + private void Update() + { + InvalidateVisual(); + } + + private void UpdateTransform() + { + InvalidateVisual(); + InvalidateMeasure(); + } + + private Rect MeasureDevice() + { + if (Device == null) + return Rect.Empty; + + Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height); + Geometry geometry = new RectangleGeometry(deviceRect); + geometry.Transform = new RotateTransform(Device.Rotation); + + return geometry.Bounds; + } + + private void TimerOnTick(object? sender, EventArgs e) + { + if (ShowColors && IsVisible && Opacity > 0) + Update(); + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (Device == null) + return; + + Point position = e.GetPosition(this); + double x = position.X / Bounds.Width; + double y = position.Y / Bounds.Height; + + Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height); + DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.HitTest(scaledPosition)); + if (deviceVisualizerLed != null) + OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led)); + } + + private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + Dispatcher.UIThread.Post(SetupForDevice); + } + + private void DeviceUpdated(object? sender, EventArgs e) + { + Dispatcher.UIThread.Post(SetupForDevice); + } + #region Properties /// @@ -67,26 +185,6 @@ namespace Artemis.UI.Avalonia.Shared.Controls #endregion - private readonly DispatcherTimer _timer; - - /// - public DeviceVisualizer() - { - // Run an update timer at 25 fps - _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)}; - } - - /// - public override void Render(DrawingContext context) - { - base.Render(context); - } - - private void Update() - { - throw new NotImplementedException(); - } - #region Lifetime management /// @@ -105,14 +203,37 @@ namespace Artemis.UI.Avalonia.Shared.Controls base.OnDetachedFromLogicalTree(e); } - #endregion - - #region Event handlers - - private void TimerOnTick(object? sender, EventArgs e) + private void SetupForDevice() { - if (ShowColors && IsVisible && Opacity > 0) - Update(); + _deviceImage = null; + _deviceVisualizerLeds.Clear(); + _highlightedLeds = new List(); + _dimmedLeds = new List(); + + if (Device == null) + return; + + if (_oldDevice != null) + { + Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; + Device.DeviceUpdated -= DeviceUpdated; + } + + _oldDevice = Device; + + Device.RgbDevice.PropertyChanged += DevicePropertyChanged; + Device.DeviceUpdated += DeviceUpdated; + UpdateTransform(); + + // Load the device main image + if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath)) + _deviceImage = new Bitmap(Device.Layout.Image.AbsolutePath); + + // Create all the LEDs + foreach (ArtemisLed artemisLed in Device.Leds) + _deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed)); + + InvalidateMeasure(); } #endregion diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs new file mode 100644 index 000000000..ae6c6ff5e --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs @@ -0,0 +1,159 @@ +using System; +using System.IO; +using Artemis.Core; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using RGB.NET.Core; +using Color = Avalonia.Media.Color; +using Point = Avalonia.Point; +using SolidColorBrush = Avalonia.Media.SolidColorBrush; + +namespace Artemis.UI.Avalonia.Shared.Controls +{ + internal class DeviceVisualizerLed + { + private const byte Dimmed = 100; + private const byte NonDimmed = 255; + + public DeviceVisualizerLed(ArtemisLed led) + { + Led = led; + LedRect = new Rect( + Led.RgbLed.Location.X, + Led.RgbLed.Location.Y, + Led.RgbLed.Size.Width, + Led.RgbLed.Size.Height + ); + + if (Led.Layout?.Image != null && File.Exists(Led.Layout.Image.LocalPath)) + LedImage = new Bitmap(Led.Layout.Image.AbsolutePath); + + CreateLedGeometry(); + } + + public ArtemisLed Led { get; } + public Rect LedRect { get; set; } + public Bitmap? LedImage { get; set; } + public Geometry? DisplayGeometry { get; private set; } + + public void RenderImage(DrawingContext drawingContext) + { + if (LedImage == null) + return; + + drawingContext.DrawImage(LedImage, LedRect); + + byte r = Led.RgbLed.Color.GetR(); + byte g = Led.RgbLed.Color.GetG(); + byte b = Led.RgbLed.Color.GetB(); + SolidColorBrush fillBrush = new(new Color(100, r, g, b)); + SolidColorBrush penBrush = new(new Color(255, r, g, b)); + + // Create transparent pixels covering the entire LedRect so the image size matched the LedRect size + // drawingContext.DrawRectangle(new SolidColorBrush(Colors.Transparent), new Pen(new SolidColorBrush(Colors.Transparent), 1), LedRect); + // Translate to the top-left of the LedRect + using DrawingContext.PushedState push = drawingContext.PushPostTransform(Matrix.CreateTranslation(LedRect.X, LedRect.Y)); + // Render the LED geometry + drawingContext.DrawGeometry(fillBrush, new Pen(penBrush) {LineJoin = PenLineJoin.Round}, DisplayGeometry); + } + + public bool HitTest(Point position) + { + if (DisplayGeometry == null) + return false; + + Geometry translatedGeometry = DisplayGeometry.Clone(); + translatedGeometry.Transform = new TranslateTransform(Led.RgbLed.Location.X, Led.RgbLed.Location.Y); + return translatedGeometry.FillContains(position); + } + + private void CreateLedGeometry() + { + // The minimum required size for geometry to be created + if (Led.RgbLed.Size.Width < 2 || Led.RgbLed.Size.Height < 2) + return; + + switch (Led.RgbLed.Shape) + { + case Shape.Custom: + if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad) + CreateCustomGeometry(2.0); + else + CreateCustomGeometry(1.0); + break; + case Shape.Rectangle: + if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad) + CreateKeyCapGeometry(); + else + CreateRectangleGeometry(); + break; + case Shape.Circle: + CreateCircleGeometry(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private void CreateRectangleGeometry() + { + DisplayGeometry = new RectangleGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); + } + + private void CreateCircleGeometry() + { + DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); + } + + private void CreateKeyCapGeometry() + { + PathGeometry path = PathGeometry.Parse($"M1,1" + + $"h{Led.RgbLed.Size.Width - 2} a10," + + $"10 0 0 1 10," + + $"10 v{Led.RgbLed.Size.Height - 2} a10," + + $"10 0 0 1 -10," + + $"10 h-{Led.RgbLed.Size.Width - 2} a10," + + $"10 0 0 1 -10," + + $"-10 v-{Led.RgbLed.Size.Height - 2} a10," + + $"10 0 0 1 10,-10 z"); + DisplayGeometry = path; + } + + private void CreateCustomGeometry(double deflateAmount) + { + try + { + double width = Led.RgbLed.Size.Width - deflateAmount; + double height = Led.RgbLed.Size.Height - deflateAmount; + + Geometry geometry = Geometry.Parse(Led.RgbLed.ShapeData); + geometry.Transform = new ScaleTransform(width, height); + geometry = geometry.Clone(); + geometry.Transform = new TranslateTransform(deflateAmount / 2, deflateAmount / 2); + DisplayGeometry = geometry.Clone(); + + // TODO: Figure out wtf was going on here + // if (DisplayGeometry.Bounds.Width > width) + // { + // DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup + // { + // Children = new TransformCollection {new ScaleTransform(width / DisplayGeometry.Bounds.Width, 1)} + // }); + // } + // + // if (DisplayGeometry.Bounds.Height > height) + // { + // DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup + // { + // Children = new TransformCollection {new ScaleTransform(1, height / DisplayGeometry.Bounds.Height)} + // }); + // } + } + catch (Exception) + { + CreateRectangleGeometry(); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs new file mode 100644 index 000000000..596f7a3c2 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs @@ -0,0 +1,21 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data about selection events raised by + /// + public class DataModelInputDynamicEventArgs : EventArgs + { + internal DataModelInputDynamicEventArgs(DataModelPath? dataModelPath) + { + DataModelPath = dataModelPath; + } + + /// + /// Gets the data model path that was selected + /// + public DataModelPath? DataModelPath { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs new file mode 100644 index 000000000..2224d5021 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data about submit events raised by + /// + public class DataModelInputStaticEventArgs : EventArgs + { + internal DataModelInputStaticEventArgs(object? value) + { + Value = value; + } + + /// + /// The value that was submitted + /// + public object? Value { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs new file mode 100644 index 000000000..e6f763a25 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs @@ -0,0 +1,27 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data on LED click events raised by the device visualizer + /// + public class LedClickedEventArgs : EventArgs + { + internal LedClickedEventArgs(ArtemisDevice device, ArtemisLed led) + { + Device = device; + Led = led; + } + + /// + /// The device that was clicked + /// + public ArtemisDevice Device { get; set; } + + /// + /// The LED that was clicked + /// + public ArtemisLed Led { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs new file mode 100644 index 000000000..ce9e24a5d --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data on profile related events raised by the profile editor + /// + public class ProfileConfigurationEventArgs : EventArgs + { + internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration) + { + ProfileConfiguration = profileConfiguration; + } + + internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration, ProfileConfiguration? previousProfileConfiguration) + { + ProfileConfiguration = profileConfiguration; + PreviousProfileConfiguration = previousProfileConfiguration; + } + + /// + /// Gets the profile the event was raised for + /// + public ProfileConfiguration? ProfileConfiguration { get; } + + /// + /// If applicable, the previous active profile before the event was raised + /// + public ProfileConfiguration? PreviousProfileConfiguration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs new file mode 100644 index 000000000..95455d818 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + /// + /// Provides data on profile element related events raised by the profile editor + /// + public class RenderProfileElementEventArgs : EventArgs + { + internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement) + { + RenderProfileElement = renderProfileElement; + } + + internal RenderProfileElementEventArgs(RenderProfileElement? renderProfileElement, RenderProfileElement? previousRenderProfileElement) + { + RenderProfileElement = renderProfileElement; + PreviousRenderProfileElement = previousRenderProfileElement; + } + + /// + /// Gets the profile element the event was raised for + /// + public RenderProfileElement? RenderProfileElement { get; } + + /// + /// If applicable, the previous active profile element before the event was raised + /// + public RenderProfileElement? PreviousRenderProfileElement { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/App.axaml b/src/Artemis.UI.Avalonia/App.axaml index bc3f412c4..0daed3338 100644 --- a/src/Artemis.UI.Avalonia/App.axaml +++ b/src/Artemis.UI.Avalonia/App.axaml @@ -11,8 +11,13 @@ - - + + + + + + + diff --git a/src/Artemis.UI.Avalonia/App.axaml.cs b/src/Artemis.UI.Avalonia/App.axaml.cs index de78f841c..345424826 100644 --- a/src/Artemis.UI.Avalonia/App.axaml.cs +++ b/src/Artemis.UI.Avalonia/App.axaml.cs @@ -1,10 +1,10 @@ using Artemis.Core.Ninject; using Artemis.UI.Avalonia.Ninject; -using Artemis.UI.Avalonia.Screens.Main.Views; using Artemis.UI.Avalonia.Screens.Root.ViewModels; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using FluentAvalonia.Styling; using Ninject; using Splat.Ninject; @@ -23,10 +23,13 @@ namespace Artemis.UI.Avalonia public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { desktop.MainWindow = new MainWindow { DataContext = _kernel.Get() }; + AvaloniaLocator.Current.GetService().ForceNativeTitleBarToTheme(desktop.MainWindow, "Dark"); + } base.OnFrameworkInitializationCompleted(); } diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Artemis.UI.Avalonia/MainWindow.axaml index ee8375848..2b5967a0c 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml @@ -4,11 +4,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Main.Views.MainWindow" + x:Class="Artemis.UI.Avalonia.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="Artemis.UI.Avalonia" + ExtendClientAreaToDecorationsHint="True" TransparencyLevelHint="AcrylicBlur" Background="Transparent" - ExtendClientAreaToDecorationsHint="True" Content="{Binding}"> \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs index be13f759e..d57321c31 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs @@ -3,7 +3,7 @@ using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Main.Views +namespace Artemis.UI.Avalonia { public partial class MainWindow : ReactiveWindow { diff --git a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs b/src/Artemis.UI.Avalonia/Ninject/UIModule.cs index 74f829d80..a934f3b91 100644 --- a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs +++ b/src/Artemis.UI.Avalonia/Ninject/UIModule.cs @@ -3,6 +3,7 @@ using Artemis.UI.Avalonia.Ninject.Factories; using Artemis.UI.Avalonia.Screens; using Ninject.Extensions.Conventions; using Ninject.Modules; +using Ninject.Planning.Bindings.Resolvers; namespace Artemis.UI.Avalonia.Ninject { @@ -13,6 +14,7 @@ namespace Artemis.UI.Avalonia.Ninject if (Kernel == null) throw new ArgumentNullException("Kernel shouldn't be null here."); + Kernel.Components.Add(); Kernel.Bind(x => { diff --git a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs index 53783ae7b..c91071b45 100644 --- a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.Home.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Home.ViewModels { public class HomeViewModel : MainScreenViewModel { - public HomeViewModel() + public HomeViewModel(IScreen hostScreens) : base(hostScreens, "home") { DisplayName = "Home"; } diff --git a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs index 7ad2e7bcd..466d7b0c0 100644 --- a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs @@ -1,7 +1,19 @@ -namespace Artemis.UI.Avalonia.Screens +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens { - public class MainScreenViewModel : ViewModelBase + public abstract class MainScreenViewModel : ViewModelBase, IRoutableViewModel { - + protected MainScreenViewModel(IScreen hostScreen, string urlPathSegment) + { + HostScreen = hostScreen; + UrlPathSegment = urlPathSegment; + } + + /// + public string UrlPathSegment { get; } + + /// + public IScreen HostScreen { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs index 2645dc999..376906861 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive; using Artemis.Core.Services; using ReactiveUI; @@ -10,18 +11,20 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public RootViewModel(ICoreService coreService, SidebarViewModel sidebarViewModel) { + Router = new RoutingState(); SidebarViewModel = sidebarViewModel; + SidebarViewModel.Router = Router; + _coreService = coreService; _coreService.Initialize(); - Console.WriteLine("test"); } public SidebarViewModel SidebarViewModel { get; } - + /// public ViewModelActivator Activator { get; } = new(); /// - public RoutingState Router { get; } = new(); + public RoutingState Router { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs index 5d8d7e11a..2b2769587 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs @@ -1,5 +1,8 @@ -using Material.Icons; +using System; +using System.Reactive.Linq; +using Material.Icons; using Ninject; +using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { @@ -27,5 +30,10 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public string DisplayName { get; } public abstract MainScreenViewModel CreateInstance(IKernel kernel); + + public bool IsActive(IObservable routerCurrentViewModel) + { + return false; + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs index 80a0340f0..a8b63697a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs @@ -10,6 +10,7 @@ using Artemis.UI.Avalonia.Screens.Workshop.ViewModels; using Material.Icons; using Ninject; using ReactiveUI; +using RGB.NET.Core; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { @@ -17,13 +18,18 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { private readonly IKernel _kernel; private readonly IProfileService _profileService; + private readonly IRgbService _rgbService; private readonly ISidebarVmFactory _sidebarVmFactory; - private SidebarScreenViewModel _selectedSidebarScreen; + private ArtemisDevice? _headerDevice; - public SidebarViewModel(IKernel kernel, IProfileService profileService, ISidebarVmFactory sidebarVmFactory) + private SidebarScreenViewModel _selectedSidebarScreen; + private RoutingState _router; + + public SidebarViewModel(IKernel kernel, IProfileService profileService, IRgbService rgbService, ISidebarVmFactory sidebarVmFactory) { _kernel = kernel; _profileService = profileService; + _rgbService = rgbService; _sidebarVmFactory = sidebarVmFactory; SidebarScreens = new ObservableCollection @@ -33,17 +39,43 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels new SidebarScreenViewModel(MaterialIconKind.Devices, "Surface Editor"), new SidebarScreenViewModel(MaterialIconKind.Cog, "Settings") }; - SelectedSidebarScreen = SidebarScreens.First(); + _selectedSidebarScreen = SidebarScreens.First(); + UpdateProfileCategories(); + UpdateHeaderDevice(); } public ObservableCollection SidebarScreens { get; } public ObservableCollection SidebarCategories { get; } = new(); - + + public ArtemisDevice? HeaderDevice + { + get => _headerDevice; + set => this.RaiseAndSetIfChanged(ref _headerDevice, value); + } + public SidebarScreenViewModel SelectedSidebarScreen { get => _selectedSidebarScreen; - set => this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); + set + { + this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); + // if (!SelectedSidebarScreen.IsActive(Router.CurrentViewModel)) + // Router.Navigate.Execute(SelectedSidebarScreen.CreateInstance(_kernel)).Subscribe(); + } + } + + public RoutingState Router + { + get => _router; + set => this.RaiseAndSetIfChanged(ref _router, value); + } + + public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) + { + SidebarCategoryViewModel viewModel = _sidebarVmFactory.SidebarCategoryViewModel(profileCategory); + SidebarCategories.Add(viewModel); + return viewModel; } private void UpdateProfileCategories() @@ -53,11 +85,9 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels AddProfileCategoryViewModel(profileCategory); } - public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) + private void UpdateHeaderDevice() { - SidebarCategoryViewModel viewModel = _sidebarVmFactory.SidebarCategoryViewModel(profileCategory); - SidebarCategories.Add(viewModel); - return viewModel; + HeaderDevice = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard && d.Layout is {IsValid: true}); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index 0d4df056b..472555f77 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -1,10 +1,10 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:reactiveUi="http://reactiveui.net" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="Artemis.UI.Avalonia.Screens.Root.Views.RootView"> @@ -19,11 +19,10 @@ TintOpacity="1" MaterialOpacity="0.65" /> - + - - - + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml index 44e0b35c1..5dc95cbc8 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml @@ -4,7 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" + xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" + mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Root.Views.SidebarView"> @@ -16,40 +18,41 @@ - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - + + - + + + + + Artemis 2 diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs index 116b88016..30c9fde1a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels { public class SettingsViewModel : MainScreenViewModel { - public SettingsViewModel() + public SettingsViewModel(IScreen hostScreens) : base(hostScreens, "settings") { DisplayName = "Settings"; } diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs index dfbf164bd..c97d12ed8 100644 --- a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels { public class SurfaceEditorViewModel : MainScreenViewModel { - public SurfaceEditorViewModel() + public SurfaceEditorViewModel(IScreen hostScreens) : base(hostScreens, "surface-editor") { DisplayName = "Surface Editor"; } diff --git a/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs index f645047c9..28eb2caef 100644 --- a/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs @@ -1,8 +1,10 @@ -namespace Artemis.UI.Avalonia.Screens.Workshop.ViewModels +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Workshop.ViewModels { public class WorkshopViewModel : MainScreenViewModel { - public WorkshopViewModel() + public WorkshopViewModel(IScreen hostScreens) : base(hostScreens, "workshop") { DisplayName = "Workshop"; } From d062ceaf05482bd9b6bc39def77fa184828b5730 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 11 Oct 2021 22:56:42 +0200 Subject: [PATCH 050/270] UI - Implemented routing --- src/Artemis.UI.Avalonia/App.axaml | 20 ++- .../Artemis.UI.Avalonia.csproj | 146 +++++++++--------- .../ColorToSolidColorBrushConverter.cs | 1 - .../Converters/EnumToCollectionConverter.cs | 21 ++- .../Converters/ValuesAdditionConverter.cs | 1 - src/Artemis.UI.Avalonia/MainWindow.axaml | 1 - src/Artemis.UI.Avalonia/MainWindow.axaml.cs | 2 +- .../Ninject/Factories/IVMFactory.cs | 2 + src/Artemis.UI.Avalonia/Program.cs | 22 +-- .../Screens/Home/ViewModels/HomeViewModel.cs | 7 +- .../Screens/Home/Views/HomeView.axaml | 8 + .../Screens/Home/Views/HomeView.axaml.cs | 19 +++ .../Screens/MainScreenViewModel.cs | 2 +- .../ViewModels/ProfileEditorViewModel.cs | 6 + .../Views/ProfileEditorView.axaml | 8 + .../Views/ProfileEditorView.axaml.cs | 19 +++ .../Screens/Root/ViewModels/RootViewModel.cs | 15 +- .../Root/ViewModels/SidebarScreenViewModel.cs | 11 +- .../Root/ViewModels/SidebarViewModel.cs | 41 ++--- .../Screens/Root/Views/RootView.axaml | 7 +- .../Root/Views/SidebarCategoryView.axaml | 4 +- .../SidebarProfileConfigurationView.axaml | 19 ++- .../Root/Views/SidebarScreenView.axaml | 6 +- .../Screens/Root/Views/SidebarView.axaml | 28 ++-- .../Screens/Root/Views/SidebarView.axaml.cs | 5 +- .../ViewModels/SurfaceEditorViewModel.cs | 2 +- .../Screens/Settings/Views/SettingsView.axaml | 8 + .../Settings/Views/SettingsView.axaml.cs | 19 +++ .../ViewModels/SurfaceEditorViewModel.cs | 2 +- .../Views/SurfaceEditorView.axaml | 8 + .../Views/SurfaceEditorView.axaml.cs | 19 +++ .../Workshop/ViewModels/WorkshopViewModel.cs | 2 +- .../Screens/Workshop/Views/WorkshopView.axaml | 8 + .../Workshop/Views/WorkshopView.axaml.cs | 19 +++ src/Artemis.UI.Avalonia/Styles/Button.axaml | 22 +-- src/Artemis.UI.Avalonia/Styles/Sidebar.axaml | 9 +- src/Artemis.UI.Avalonia/ViewLocator.cs | 2 +- src/Artemis.UI.Avalonia/ViewModelBase.cs | 9 +- 38 files changed, 343 insertions(+), 207 deletions(-) create mode 100644 src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Workshop/Views/WorkshopView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Workshop/Views/WorkshopView.axaml.cs diff --git a/src/Artemis.UI.Avalonia/App.axaml b/src/Artemis.UI.Avalonia/App.axaml index 0daed3338..fb754aab7 100644 --- a/src/Artemis.UI.Avalonia/App.axaml +++ b/src/Artemis.UI.Avalonia/App.axaml @@ -2,25 +2,23 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Artemis.UI.Avalonia" xmlns:sty="using:FluentAvalonia.Styling" - xmlns:ui="using:FluentAvalonia.UI.Controls" - xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives" x:Class="Artemis.UI.Avalonia.App"> - + - - - + + + - + - - - + + + - + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj index 2a4fb9d50..e3b4cb988 100644 --- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj @@ -1,76 +1,72 @@  - - WinExe - net5.0 - enable - - - - - - - - - - - - - - - - - - - - - - - - - - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - SidebarView.axaml - Code - - - RootView.axaml - Code - - - - - - - - - - - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - - - - - - - - + + WinExe + net5.0 + enable + + + + + + + + + + + + + + + + + + + + + + + + %(Filename) + + + %(Filename) + + + %(Filename) + + + %(Filename) + + + %(Filename) + + + SidebarView.axaml + Code + + + RootView.axaml + Code + + + + + + + + + + + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/ColorToSolidColorBrushConverter.cs b/src/Artemis.UI.Avalonia/Converters/ColorToSolidColorBrushConverter.cs index b29bfe53a..c49188a74 100644 --- a/src/Artemis.UI.Avalonia/Converters/ColorToSolidColorBrushConverter.cs +++ b/src/Artemis.UI.Avalonia/Converters/ColorToSolidColorBrushConverter.cs @@ -14,7 +14,6 @@ namespace Artemis.UI.Avalonia.Converters /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return new SolidColorBrush(!(value is RGBColor color) ? new Color(0, 0, 0, 0) : new Color((byte) color.A, (byte) color.R, (byte) color.G, (byte) color.B)); diff --git a/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs b/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs index 1d1977444..a3e6609d1 100644 --- a/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs +++ b/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs @@ -8,19 +8,8 @@ using Avalonia.Markup.Xaml; namespace Artemis.UI.Avalonia.Converters { - public class EnumToCollectionConverter : MarkupExtension, IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return GetAllValuesAndDescriptions(value.GetType()); - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return null; - } - public override object ProvideValue(IServiceProvider serviceProvider) { return this; @@ -44,5 +33,15 @@ namespace Artemis.UI.Avalonia.Converters return Enum.GetValues(t).Cast().Select(e => new Tuple(e, Description(e))).ToList(); } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return GetAllValuesAndDescriptions(value.GetType()); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return null; + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs b/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs index 1139efa67..55fe27a36 100644 --- a/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs +++ b/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs @@ -8,7 +8,6 @@ namespace Artemis.UI.Avalonia.Converters { public class ValuesAdditionConverter : IMultiValueConverter { - /// public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Artemis.UI.Avalonia/MainWindow.axaml index 2b5967a0c..2472b6c84 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.MainWindow" Icon="/Assets/avalonia-logo.ico" diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs index d57321c31..b6c98ac3f 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml.cs @@ -5,7 +5,7 @@ using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia { - public partial class MainWindow : ReactiveWindow + public class MainWindow : ReactiveWindow { public MainWindow() { diff --git a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs index f393dfecb..316935f82 100644 --- a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using ReactiveUI; namespace Artemis.UI.Avalonia.Ninject.Factories { @@ -9,6 +10,7 @@ namespace Artemis.UI.Avalonia.Ninject.Factories public interface ISidebarVmFactory : IVmFactory { + SidebarViewModel SidebarViewModel(IScreen hostScreen); SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); } diff --git a/src/Artemis.UI.Avalonia/Program.cs b/src/Artemis.UI.Avalonia/Program.cs index 6d2382455..486440ee6 100644 --- a/src/Artemis.UI.Avalonia/Program.cs +++ b/src/Artemis.UI.Avalonia/Program.cs @@ -1,29 +1,29 @@ -using System; -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; +using Avalonia; using Avalonia.ReactiveUI; using Ninject; -using ReactiveUI; -using Splat; -using Splat.Ninject; namespace Artemis.UI.Avalonia { - class Program + internal class Program { private static StandardKernel _kernel; // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() + { + return AppBuilder.Configure() .UsePlatformDetect() .LogToTrace() .UseReactiveUI(); + } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs index c91071b45..46ee6aa2a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Home/ViewModels/HomeViewModel.cs @@ -4,14 +4,9 @@ namespace Artemis.UI.Avalonia.Screens.Home.ViewModels { public class HomeViewModel : MainScreenViewModel { - public HomeViewModel(IScreen hostScreens) : base(hostScreens, "home") + public HomeViewModel(IScreen hostScreen) : base(hostScreen, "home") { DisplayName = "Home"; } - - public void OpenUrl(string url) - { - Core.Utilities.OpenUrl(url); - } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml new file mode 100644 index 000000000..daac27813 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml @@ -0,0 +1,8 @@ + + Home :> + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml.cs new file mode 100644 index 000000000..bb3f58517 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml.cs @@ -0,0 +1,19 @@ +using Artemis.UI.Avalonia.Screens.Home.ViewModels; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Home.Views +{ + public class HomeView : ReactiveUserControl + { + public HomeView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs index 466d7b0c0..a4e5453e4 100644 --- a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs @@ -2,7 +2,7 @@ namespace Artemis.UI.Avalonia.Screens { - public abstract class MainScreenViewModel : ViewModelBase, IRoutableViewModel + public abstract class MainScreenViewModel : ActivatableViewModelBase, IRoutableViewModel { protected MainScreenViewModel(IScreen hostScreen, string urlPathSegment) { diff --git a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs new file mode 100644 index 000000000..0e10cd877 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs @@ -0,0 +1,6 @@ +namespace Artemis.UI.Avalonia.Screens.ProfileEditor.ViewModels +{ + public class ProfileEditorViewModel : ActivatableViewModelBase + { + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml new file mode 100644 index 000000000..1db8610af --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs new file mode 100644 index 000000000..5dc3141ed --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs @@ -0,0 +1,19 @@ +using Artemis.UI.Avalonia.Screens.ProfileEditor.ViewModels; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.ProfileEditor.Views +{ + public class ProfileEditorView : ReactiveUserControl + { + public ProfileEditorView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs index 376906861..e3110a5d5 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs @@ -1,19 +1,17 @@ -using System; -using System.Reactive; -using Artemis.Core.Services; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { - public class RootViewModel : ViewModelBase, IScreen, IActivatableViewModel + public class RootViewModel : ActivatableViewModelBase, IScreen { private readonly ICoreService _coreService; - public RootViewModel(ICoreService coreService, SidebarViewModel sidebarViewModel) + public RootViewModel(ICoreService coreService, ISidebarVmFactory sidebarVmFactory) { Router = new RoutingState(); - SidebarViewModel = sidebarViewModel; - SidebarViewModel.Router = Router; + SidebarViewModel = sidebarVmFactory.SidebarViewModel(this); _coreService = coreService; _coreService.Initialize(); @@ -21,9 +19,6 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public SidebarViewModel SidebarViewModel { get; } - /// - public ViewModelActivator Activator { get; } = new(); - /// public RoutingState Router { get; } } diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs index 2b2769587..0a8b89867 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs @@ -1,7 +1,7 @@ using System; -using System.Reactive.Linq; using Material.Icons; using Ninject; +using Ninject.Parameters; using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels @@ -12,9 +12,11 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { } - public override MainScreenViewModel CreateInstance(IKernel kernel) + public override Type ScreenType => typeof(T); + + public override MainScreenViewModel CreateInstance(IKernel kernel, IScreen screen) { - return kernel.Get(); + return kernel.Get(new ConstructorArgument("hostScreen", screen)); } } @@ -29,7 +31,8 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public MaterialIconKind Icon { get; } public string DisplayName { get; } - public abstract MainScreenViewModel CreateInstance(IKernel kernel); + public abstract Type ScreenType { get; } + public abstract MainScreenViewModel CreateInstance(IKernel kernel, IScreen screen); public bool IsActive(IObservable routerCurrentViewModel) { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs index a8b63697a..7456da69a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs @@ -1,5 +1,7 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Avalonia.Ninject.Factories; @@ -14,20 +16,19 @@ using RGB.NET.Core; namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { - public class SidebarViewModel : ViewModelBase + public class SidebarViewModel : ActivatableViewModelBase { - private readonly IKernel _kernel; + private readonly IScreen _hostScreen; private readonly IProfileService _profileService; private readonly IRgbService _rgbService; private readonly ISidebarVmFactory _sidebarVmFactory; private ArtemisDevice? _headerDevice; - private SidebarScreenViewModel _selectedSidebarScreen; - private RoutingState _router; + private SidebarScreenViewModel? _selectedSidebarScreen; - public SidebarViewModel(IKernel kernel, IProfileService profileService, IRgbService rgbService, ISidebarVmFactory sidebarVmFactory) + public SidebarViewModel(IScreen hostScreen, IKernel kernel, IProfileService profileService, IRgbService rgbService, ISidebarVmFactory sidebarVmFactory) { - _kernel = kernel; + _hostScreen = hostScreen; _profileService = profileService; _rgbService = rgbService; _sidebarVmFactory = sidebarVmFactory; @@ -43,6 +44,17 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels UpdateProfileCategories(); UpdateHeaderDevice(); + + this.WhenActivated(disposables => + { + this.WhenAnyObservable(vm => vm._hostScreen.Router.CurrentViewModel) + .WhereNotNull() + .Subscribe(c => SelectedSidebarScreen = SidebarScreens.FirstOrDefault(s => s.ScreenType == c.GetType())) + .DisposeWith(disposables); + this.WhenAnyValue(vm => vm.SelectedSidebarScreen) + .WhereNotNull() + .Subscribe(s => _hostScreen.Router.Navigate.Execute(s.CreateInstance(kernel, _hostScreen))); + }); } public ObservableCollection SidebarScreens { get; } @@ -54,21 +66,10 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels set => this.RaiseAndSetIfChanged(ref _headerDevice, value); } - public SidebarScreenViewModel SelectedSidebarScreen + public SidebarScreenViewModel? SelectedSidebarScreen { get => _selectedSidebarScreen; - set - { - this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); - // if (!SelectedSidebarScreen.IsActive(Router.CurrentViewModel)) - // Router.Navigate.Execute(SelectedSidebarScreen.CreateInstance(_kernel)).Subscribe(); - } - } - - public RoutingState Router - { - get => _router; - set => this.RaiseAndSetIfChanged(ref _router, value); + set => this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); } public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index 472555f77..13ae28f59 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -23,6 +23,11 @@ - + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml index bd9fc5c88..d6d3b7633 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml @@ -8,10 +8,10 @@ x:Class="Artemis.UI.Avalonia.Screens.Root.Views.SidebarCategoryView"> - + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml b/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml index bfed7dcec..e4d4ac6b1 100644 --- a/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml +++ b/src/Artemis.UI.Avalonia/Styles/Sidebar.axaml @@ -1,6 +1,5 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -14,7 +13,7 @@ - + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/ViewLocator.cs b/src/Artemis.UI.Avalonia/ViewLocator.cs index 34e508620..b4d3fafd1 100644 --- a/src/Artemis.UI.Avalonia/ViewLocator.cs +++ b/src/Artemis.UI.Avalonia/ViewLocator.cs @@ -15,7 +15,7 @@ namespace Artemis.UI.Avalonia if (type != null) return (Control) Activator.CreateInstance(type)!; - return new TextBlock { Text = "Not Found: " + name }; + return new TextBlock {Text = "Not Found: " + name}; } public bool Match(object data) diff --git a/src/Artemis.UI.Avalonia/ViewModelBase.cs b/src/Artemis.UI.Avalonia/ViewModelBase.cs index 38fbea65d..b7cf2eb1d 100644 --- a/src/Artemis.UI.Avalonia/ViewModelBase.cs +++ b/src/Artemis.UI.Avalonia/ViewModelBase.cs @@ -2,7 +2,7 @@ namespace Artemis.UI.Avalonia { - public class ViewModelBase : ReactiveObject + public abstract class ViewModelBase : ReactiveObject { private string? _displayName; @@ -12,4 +12,9 @@ namespace Artemis.UI.Avalonia set => this.RaiseAndSetIfChanged(ref _displayName, value); } } -} + + public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel + { + public ViewModelActivator Activator { get; } = new(); + } +} \ No newline at end of file From ef52455db172bc2589cd36573bf59340a8dd5b8f Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 12 Oct 2021 23:51:25 +0200 Subject: [PATCH 051/270] UI - Added home screen --- src/Artemis.UI.Avalonia/App.axaml | 1 + .../Artemis.UI.Avalonia.csproj | 3 + .../Assets/Images/home-banner.png | Bin 0 -> 579525 bytes src/Artemis.UI.Avalonia/MainWindow.axaml | 17 ++- .../Screens/Home/Views/HomeView.axaml | 136 +++++++++++++++++- .../Root/ViewModels/SidebarScreenViewModel.cs | 5 - .../Screens/Root/Views/RootView.axaml | 27 ++-- .../Screens/Root/Views/SidebarView.axaml | 21 --- src/Artemis.UI.Avalonia/Styles/Border.axaml | 24 ++++ src/Artemis.UI/Screens/Home/HomeView.xaml | 2 +- 10 files changed, 188 insertions(+), 48 deletions(-) create mode 100644 src/Artemis.UI.Avalonia/Assets/Images/home-banner.png create mode 100644 src/Artemis.UI.Avalonia/Styles/Border.axaml diff --git a/src/Artemis.UI.Avalonia/App.axaml b/src/Artemis.UI.Avalonia/App.axaml index fb754aab7..816b5fd50 100644 --- a/src/Artemis.UI.Avalonia/App.axaml +++ b/src/Artemis.UI.Avalonia/App.axaml @@ -18,6 +18,7 @@ + diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj index e3b4cb988..34818821e 100644 --- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj @@ -9,6 +9,9 @@ + + + diff --git a/src/Artemis.UI.Avalonia/Assets/Images/home-banner.png b/src/Artemis.UI.Avalonia/Assets/Images/home-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..a0eb5d65fbfd4db93f6de571a98cbf19e70c286d GIT binary patch literal 579525 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

@{_EvNnWvj5 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

7Wg;Hj5XDq!TOAx^{sO(dWc#mkHW0 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

8bG# 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 JPDHLkV1izUsYCz( literal 0 HcmV?d00001 diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Artemis.UI.Avalonia/MainWindow.axaml index 2472b6c84..484b6a22b 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml @@ -8,6 +8,19 @@ Title="Artemis.UI.Avalonia" ExtendClientAreaToDecorationsHint="True" TransparencyLevelHint="AcrylicBlur" - Background="Transparent" - Content="{Binding}"> + Background="Transparent"> + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml index daac27813..6a2506586 100644 --- a/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml @@ -2,7 +2,139 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="900" x:Class="Artemis.UI.Avalonia.Screens.Home.Views.HomeView"> - Home :> + + + + + + + + + + + + + + + + + + + + + Plugins + + Artemis is built up using plugins. This means devices, brushes, effects and modules (for supporting games!) can all be added via plugins. + + + Under Settings > Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers. + + + We're also keeping track of a list of third-party plugins on our wiki. + + + + + + + Get more plugins + + + + + + + + + + + Have a chat + + If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels. + + + + + + + + + GitHub + + + + + + Website + + + + + + Discord + + + + + + E-mail + + + + + + + + + + + + Open Source + + This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, I like numbers. You could even make plugins, there's a full documentation on the website + + + + + + + + Donate + + + + Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs index 0a8b89867..051393e44 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs @@ -33,10 +33,5 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels public abstract Type ScreenType { get; } public abstract MainScreenViewModel CreateInstance(IKernel kernel, IScreen screen); - - public bool IsActive(IObservable routerCurrentViewModel) - { - return false; - } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index 13ae28f59..51f17fcc9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -5,29 +5,22 @@ xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Root.Views.RootView"> - - - - - - - - + - - - - - + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml index 747b22e92..9210a465a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml @@ -24,27 +24,6 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Artemis.UI.Avalonia/Styles/Border.axaml b/src/Artemis.UI.Avalonia/Styles/Border.axaml new file mode 100644 index 000000000..99084de91 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Styles/Border.axaml @@ -0,0 +1,24 @@ + + + + + I'm in a panel yo! + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Home/HomeView.xaml b/src/Artemis.UI/Screens/Home/HomeView.xaml index 1f6d99cc9..d5be8da8f 100644 --- a/src/Artemis.UI/Screens/Home/HomeView.xaml +++ b/src/Artemis.UI/Screens/Home/HomeView.xaml @@ -27,7 +27,7 @@ - + From 491a0bdbc3aa64c62c3384a1f13b35ead37b2a34 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 16 Oct 2021 20:05:02 +0200 Subject: [PATCH 052/270] UI - Settings screen and About tab --- src/Artemis.UI.Avalonia/App.axaml | 1 + .../Artemis.UI.Avalonia.csproj | 1 + src/Artemis.UI.Avalonia/MainWindow.axaml | 2 +- .../Settings/ViewModels/AboutTabViewModel.cs | 74 +++++ .../ViewModels/DevicesTabViewModel.cs | 16 ++ .../ViewModels/GeneralTabViewModel.cs | 16 ++ .../ViewModels/PluginsTabViewModel.cs | 16 ++ .../Settings/ViewModels/SettingsViewModel.cs | 25 ++ .../ViewModels/SurfaceEditorViewModel.cs | 12 - .../Screens/Settings/Views/AboutTabView.axaml | 270 ++++++++++++++++++ .../Settings/Views/AboutTabView.axaml.cs | 21 ++ .../Settings/Views/DevicesTabView.axaml | 8 + .../Settings/Views/DevicesTabView.axaml.cs | 19 ++ .../Settings/Views/GeneralTabView.axaml | 8 + .../Settings/Views/GeneralTabView.axaml.cs | 19 ++ .../Settings/Views/PluginsTabView.axaml | 8 + .../Settings/Views/PluginsTabView.axaml.cs | 19 ++ .../Screens/Settings/Views/SettingsView.axaml | 13 +- src/Artemis.UI.Avalonia/Styles/Border.axaml | 36 ++- src/Artemis.UI.Avalonia/Styles/Button.axaml | 2 +- .../Styles/TextBlock.axaml | 39 +++ src/Artemis.UI.Avalonia/packages.lock.json | 16 ++ 22 files changed, 615 insertions(+), 26 deletions(-) create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs delete mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Styles/TextBlock.axaml diff --git a/src/Artemis.UI.Avalonia/App.axaml b/src/Artemis.UI.Avalonia/App.axaml index 816b5fd50..bce72106b 100644 --- a/src/Artemis.UI.Avalonia/App.axaml +++ b/src/Artemis.UI.Avalonia/App.axaml @@ -20,6 +20,7 @@ + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj index 34818821e..71dc1529f 100644 --- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Artemis.UI.Avalonia/MainWindow.axaml index 484b6a22b..e24aa179c 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml +++ b/src/Artemis.UI.Avalonia/MainWindow.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.MainWindow" - Icon="/Assets/avalonia-logo.ico" + Icon="/Assets/Images/Logo/bow.ico" Title="Artemis.UI.Avalonia" ExtendClientAreaToDecorationsHint="True" TransparencyLevelHint="AcrylicBlur" diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs new file mode 100644 index 000000000..67c817382 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs @@ -0,0 +1,74 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Artemis.Core; +using Avalonia.Media.Imaging; +using Flurl.Http; +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +{ + public class AboutTabViewModel : ActivatableViewModelBase + { + private Bitmap? _darthAffeProfileImage; + private Bitmap? _drMeteorProfileImage; + private Bitmap? _kaiProfileImage; + private Bitmap? _robertProfileImage; + private string? _version; + + public AboutTabViewModel() + { + DisplayName = "About"; + this.WhenActivated((Action _) => Task.Run(Activate)); + } + + public string? Version + { + get => _version; + set => this.RaiseAndSetIfChanged(ref _version, value); + } + + public Bitmap? RobertProfileImage + { + get => _robertProfileImage; + set => this.RaiseAndSetIfChanged(ref _robertProfileImage, value); + } + + public Bitmap? DarthAffeProfileImage + { + get => _darthAffeProfileImage; + set => this.RaiseAndSetIfChanged(ref _darthAffeProfileImage, value); + } + + public Bitmap? DrMeteorProfileImage + { + get => _drMeteorProfileImage; + set => this.RaiseAndSetIfChanged(ref _drMeteorProfileImage, value); + } + + public Bitmap? KaiProfileImage + { + get => _kaiProfileImage; + set => this.RaiseAndSetIfChanged(ref _kaiProfileImage, value); + } + + private async Task Activate() + { + AssemblyInformationalVersionAttribute? versionAttribute = typeof(AboutTabViewModel).Assembly.GetCustomAttribute(); + Version = $"Version {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}"; + + try + { + RobertProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/8858506".GetStreamAsync()); + RobertProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/8858506".GetStreamAsync()); + DarthAffeProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/1094841".GetStreamAsync()); + DrMeteorProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/29486064".GetStreamAsync()); + KaiProfileImage = new Bitmap(await "https://i.imgur.com/8mPWY1j.png".GetStreamAsync()); + } + catch (Exception) + { + // ignored, unluckyyyy + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs new file mode 100644 index 000000000..b975103bc --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +{ + public class DevicesTabViewModel : ActivatableViewModelBase + { + public DevicesTabViewModel() + { + DisplayName = "Devices"; + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs new file mode 100644 index 000000000..340935a80 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +{ + public class GeneralTabViewModel : ActivatableViewModelBase + { + public GeneralTabViewModel() + { + DisplayName = "General"; + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs new file mode 100644 index 000000000..3caf01598 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +{ + public class PluginsTabViewModel : ActivatableViewModelBase + { + public PluginsTabViewModel() + { + DisplayName = "Plugins"; + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs new file mode 100644 index 000000000..68fc97f3f --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs @@ -0,0 +1,25 @@ +using System.Collections.ObjectModel; +using ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels +{ + public class SettingsViewModel : MainScreenViewModel + { + public SettingsViewModel(IScreen hostScreen, + GeneralTabViewModel generalTabViewModel, + PluginsTabViewModel pluginsTabViewModel, + DevicesTabViewModel devicesTabViewModel, + AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings") + { + SettingTabs = new ObservableCollection + { + generalTabViewModel, + pluginsTabViewModel, + devicesTabViewModel, + aboutTabViewModel + }; + } + + public ObservableCollection SettingTabs { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs deleted file mode 100644 index 7b9ff1400..000000000 --- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SurfaceEditorViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ReactiveUI; - -namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels -{ - public class SettingsViewModel : MainScreenViewModel - { - public SettingsViewModel(IScreen hostScreen) : base(hostScreen, "settings") - { - DisplayName = "Settings"; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml new file mode 100644 index 000000000..e618183ba --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml @@ -0,0 +1,270 @@ + + + + + + + + + + + Artemis 2 + + + + + + + + + + + + + + + + + + PolyForm Noncommercial License 1.0.0 + + + + + + + + + + + + + Robert Beekman + + + Project owner, main contributor + + + + + + + + + + + + + + + + + + + + + + + + + + + Darth Affe + + + RGB.NET, main contributor + + + + + + + + + + + + + + + + + + Diogo 'DrMeteor' Trindade + + + Main contributor + + + + + + + + + + + + + + + + + + Kai Werling + + + Graphics design + + + + + + + + + + + + + + + + Special Thanks + + + + + - The various people creating PRs to Artemis.Plugins and the main repository + + + - All the people on Discord providing feedback and testing + + + + + + + + + + + External Libraries + + + + + + + + + Avalonia + FluentAvalonia + EmbedIO + Furl.Http + Humanizer + LiteDB + McMaster.NETCore.Plugins + Newtonsoft.Json + Ninject + RGB.NET + Serilog + SkiaSharp + Unclassified.NetRevisionTask + + + + https://avaloniaui.net/ + + + https://github.com/amwx/FluentAvalonia + + + https://unosquare.github.io/embedio/ + + + https://flurl.dev/ + + + https://github.com/Humanizr/Humanizer + + + https://www.litedb.org/ + + + https://github.com/natemcmaster/DotNetCorePlugins + + + https://www.newtonsoft.com/json + + + http://www.ninject.org/ + + + https://github.com/DarthAffe/RGB.NET + + + https://serilog.net/ + + + https://github.com/mono/SkiaSharp + + + https://unclassified.software/en/apps/netrevisiontask + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs new file mode 100644 index 000000000..89fa25025 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs @@ -0,0 +1,21 @@ +using Artemis.UI.Avalonia.Screens.Settings.ViewModels; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Settings.Views +{ + public partial class AboutTabView : ReactiveUserControl + { + public AboutTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml new file mode 100644 index 000000000..42e20b0cd --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs new file mode 100644 index 000000000..66c566a98 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Settings.Views +{ + public partial class DevicesTabView : UserControl + { + public DevicesTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml new file mode 100644 index 000000000..1aef81e68 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs new file mode 100644 index 000000000..ed21a7720 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Settings.Views +{ + public partial class GeneralTabView : UserControl + { + public GeneralTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml new file mode 100644 index 000000000..31dce537c --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs new file mode 100644 index 000000000..93874fbf9 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Settings.Views +{ + public partial class PluginsTabView : UserControl + { + public PluginsTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml index 235c04348..e085cdb82 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml @@ -4,5 +4,16 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Settings.Views.SettingsView"> - Settings! :D + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Styles/Border.axaml b/src/Artemis.UI.Avalonia/Styles/Border.axaml index 99084de91..b48918dd5 100644 --- a/src/Artemis.UI.Avalonia/Styles/Border.axaml +++ b/src/Artemis.UI.Avalonia/Styles/Border.axaml @@ -1,24 +1,38 @@  - - + + I'm in a panel yo! - + + + I'm in a panel yo! + + I'm in a panel yo! + + + - + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Styles/Button.axaml b/src/Artemis.UI.Avalonia/Styles/Button.axaml index 42ec1a5ea..e1856b64e 100644 --- a/src/Artemis.UI.Avalonia/Styles/Button.axaml +++ b/src/Artemis.UI.Avalonia/Styles/Button.axaml @@ -4,7 +4,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> - + Button.icon-button + + + + Learn more about layouts on the wiki + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesTabView.axaml.cs new file mode 100644 index 000000000..a8b8c6e05 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesTabView.axaml.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Artemis.UI.Avalonia.Screens.Device.ViewModels; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Device.Views +{ + public partial class DevicePropertiesTabView : ReactiveUserControl + { + public DevicePropertiesTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + ViewModel?.BrowseCustomLayout(); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml index 62ef30ac7..23c5676a5 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml @@ -6,13 +6,12 @@ mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Avalonia.Screens.Device.Views.DevicePropertiesView" Title="Artemis | Device Properties" - Width="1200" - Height="800" - ExtendClientAreaToDecorationsHint="True"> - - - - + Width="1250" + Height="900" + ExtendClientAreaToDecorationsHint="True" + Padding="0 32 0 0"> + + @@ -33,7 +32,11 @@ ShowColors="True" Margin="20" /> - + @@ -43,7 +46,7 @@ - + @@ -51,8 +54,8 @@ - - + + diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/InputMappingsTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/InputMappingsTabView.axaml new file mode 100644 index 000000000..50bbb2dbb --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/InputMappingsTabView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/InputMappingsTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/InputMappingsTabView.axaml.cs new file mode 100644 index 000000000..e54cccf45 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/InputMappingsTabView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Device.Views +{ + public partial class InputMappingsTabView : UserControl + { + public InputMappingsTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs index ac1f19193..b4d5f0e2f 100644 --- a/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Workshop/ViewModels/WorkshopViewModel.cs @@ -1,12 +1,27 @@ -using ReactiveUI; +using System.Reactive; +using Artemis.UI.Avalonia.Shared.Services.Builders; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Workshop.ViewModels { public class WorkshopViewModel : MainScreenViewModel { - public WorkshopViewModel(IScreen hostScreen) : base(hostScreen, "workshop") + private readonly INotificationService _notificationService; + + public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop") { + _notificationService = notificationService; + DisplayName = "Workshop"; + ShowNotification = ReactiveCommand.Create(ExecuteShowNotification); + } + + public ReactiveCommand ShowNotification { get; set; } + + private void ExecuteShowNotification(NotificationSeverity severity) + { + _notificationService.CreateNotification().WithTitle("Test title").WithMessage("Test message").WithSeverity(severity).Show(); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Workshop/Views/WorkshopView.axaml b/src/Artemis.UI.Avalonia/Screens/Workshop/Views/WorkshopView.axaml index da819c6eb..facc02cb6 100644 --- a/src/Artemis.UI.Avalonia/Screens/Workshop/Views/WorkshopView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Workshop/Views/WorkshopView.axaml @@ -2,7 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:builders="clr-namespace:Artemis.UI.Avalonia.Shared.Services.Builders;assembly=Artemis.UI.Avalonia.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Workshop.Views.WorkshopView"> - Workshop!! :3 + + Workshop!! :3 + + + Notification tests + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml b/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml index 50d958e52..facc8aa6f 100644 --- a/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml +++ b/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml @@ -17,21 +17,27 @@ + From 19685eb51646221cd4694009d4fad223c401ff67 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 11 Nov 2021 23:38:14 +0100 Subject: [PATCH 066/270] Shared UI - Added ArtemisIcon Plugins - Added features display --- .../Artemis.UI.Avalonia.Shared.xml | 12 ++ .../Controls/ArtemisIcon.axaml | 8 ++ .../Controls/ArtemisIcon.axaml.cs | 103 ++++++++++++++++++ .../ProfileConfigurationIcon.axaml.cs | 37 ++++--- .../Plugins/Views/PluginFeatureView.axaml | 68 +++++++++++- .../Plugins/Views/PluginFeatureView.axaml.cs | 4 +- .../Plugins/Views/PluginSettingsView.axaml | 15 +-- .../Plugins/Views/PluginSettingsView.axaml.cs | 4 +- 8 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml create mode 100644 src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 30012341d..e15cb64cc 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -4,6 +4,18 @@ Artemis.UI.Avalonia.Shared + +

+ Gets or sets the currently displayed icon as either a or an pointing + to an SVG + + + + + Gets or sets the currently displayed icon as either a or an pointing + to an SVG + + Visualizes an with optional per-LED colors diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml b/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml new file mode 100644 index 000000000..18cabf7e7 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs b/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs new file mode 100644 index 000000000..e0914bb4c --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs @@ -0,0 +1,103 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Svg.Skia; +using Material.Icons; +using Material.Icons.Avalonia; + +namespace Artemis.UI.Avalonia.Shared.Controls +{ + public partial class ArtemisIcon : UserControl + { + #region Properties + + /// + /// Gets or sets the currently displayed icon as either a or an pointing + /// to an SVG + /// + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon), notifying: IconChanging); + + private static void IconChanging(IAvaloniaObject sender, bool before) + { + if (before) + ((ArtemisIcon) sender).Update(); + } + + private void Update() + { + try + { + // First look for an enum value instead of a string + if (Icon is MaterialIconKind materialIcon) + Content = new MaterialIcon {Kind = materialIcon, Width = Bounds.Width, Height = Bounds.Height }; + // If it's a string there are several options + else if (Icon is string iconString) + { + // An enum defined as a string + if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon)) + Content = new MaterialIcon {Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height}; + // An URI pointing to an SVG + else if (iconString.EndsWith(".svg")) + { + SvgSource source = new(); + source.Load(iconString); + Content = new SvgImage {Source = source}; + } + // An URI pointing to a different kind of image + else + Content = new Image {Source = new Bitmap(iconString), Width = Bounds.Width, Height = Bounds.Height }; + } + } + catch + { + Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height }; + } + } + + /// + /// Gets or sets the currently displayed icon as either a or an pointing + /// to an SVG + /// + public object? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + #endregion + + public ArtemisIcon() + { + InitializeComponent(); + DetachedFromLogicalTree += OnDetachedFromLogicalTree; + LayoutUpdated += OnLayoutUpdated; + } + + private void OnLayoutUpdated(object? sender, EventArgs e) + { + if (Content is Control contentControl) + { + contentControl.Width = Bounds.Width; + contentControl.Height = Bounds.Height; + } + } + + private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e) + { + if (Content is SvgImage svgImage) + svgImage.Source?.Dispose(); + else if (Content is Image image) + ((Bitmap) image.Source).Dispose(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs index 01d1b5d8c..83be72c21 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -40,29 +40,35 @@ namespace Artemis.UI.Avalonia.Shared.Controls PropertyChanged += OnPropertyChanged; } - private void Update() { if (ConfigurationIcon == null) return; - if (ConfigurationIcon.IconType == ProfileConfigurationIconType.SvgImage && ConfigurationIcon.FileIcon != null) + try { - SvgSource source = new(); - source.Load(ConfigurationIcon.FileIcon); - Content = new SvgImage {Source = source}; + if (ConfigurationIcon.IconType == ProfileConfigurationIconType.SvgImage && ConfigurationIcon.FileIcon != null) + { + SvgSource source = new(); + source.Load(ConfigurationIcon.FileIcon); + Content = new SvgImage {Source = source}; + } + else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon && ConfigurationIcon.MaterialIcon != null) + { + Content = Enum.TryParse(ConfigurationIcon.MaterialIcon, true, out MaterialIconKind parsedIcon) + ? new MaterialIcon {Kind = parsedIcon!} + : new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; + } + else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.BitmapImage && ConfigurationIcon.FileIcon != null) + Content = new Image {Source = new Bitmap(ConfigurationIcon.FileIcon)}; + else + Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; } - else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon && ConfigurationIcon.MaterialIcon != null) + catch { - Content = Enum.TryParse(typeof(MaterialIconKind), ConfigurationIcon.MaterialIcon, true, out object? parsedIcon) - ? new MaterialIcon {Kind = (MaterialIconKind) parsedIcon!} - : new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; - } - else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.BitmapImage && ConfigurationIcon.FileIcon != null) - Content = new Image {Source = new Bitmap(ConfigurationIcon.FileIcon)}; - else Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; + } } private void InitializeComponent() @@ -76,6 +82,11 @@ namespace Artemis.UI.Avalonia.Shared.Controls { if (ConfigurationIcon != null) ConfigurationIcon.PropertyChanged -= IconOnPropertyChanged; + + if (Content is SvgImage svgImage) + svgImage.Source?.Dispose(); + else if (Content is Image image) + ((Bitmap) image.Source).Dispose(); } private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml index fdfffda4e..9100a1af1 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml @@ -2,7 +2,71 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginFeatureView"> - Welcome to Avalonia! - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Feature enabled + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml.cs index 6abc423e8..845c1489a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml.cs @@ -1,9 +1,11 @@ +using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Plugins.Views { - public partial class PluginFeatureView : UserControl + public partial class PluginFeatureView : ReactiveUserControl { public PluginFeatureView() { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml index 279feab3c..d3fb1d745 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml @@ -4,18 +4,19 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginSettingsView"> - + diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml.cs index e000ea68a..5a4f1c677 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml.cs @@ -1,9 +1,11 @@ +using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Plugins.Views { - public partial class PluginSettingsView : UserControl + public partial class PluginSettingsView : ReactiveUserControl { public PluginSettingsView() { From 26639ca8fdc6fe326a45c07e088a62e3f5c92bb1 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 12 Nov 2021 20:39:02 +0100 Subject: [PATCH 067/270] Settings - Added plugin search --- .../Plugins/Views/PluginSettingsView.axaml | 6 +++--- .../Screens/Root/Views/RootView.axaml | 14 ++++++-------- .../Tabs/ViewModels/PluginsTabViewModel.cs | 15 +++++++++------ .../Settings/Tabs/Views/PluginsTabView.axaml | 9 ++++++--- src/Artemis.UI.Avalonia/Styles/Button.axaml | 5 +++++ 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml index d3fb1d745..20feaa0ae 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml @@ -7,7 +7,7 @@ xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginSettingsView"> - + - - diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index 016362e0f..50de48c2e 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -13,14 +13,12 @@ - - - - - - - - + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs index df9af3656..bbf898563 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs @@ -19,12 +19,12 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels { public class PluginsTabViewModel : ActivatableViewModelBase { - private readonly IPluginManagementService _pluginManagementService; private readonly INotificationService _notificationService; - private readonly IWindowService _windowService; + private readonly IPluginManagementService _pluginManagementService; private readonly ISettingsVmFactory _settingsVmFactory; - private string? _searchPluginInput; + private readonly IWindowService _windowService; private List? _instances; + private string? _searchPluginInput; public PluginsTabViewModel(IPluginManagementService pluginManagementService, INotificationService notificationService, IWindowService windowService, ISettingsVmFactory settingsVmFactory) { @@ -36,7 +36,7 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels DisplayName = "Plugins"; Plugins = new ObservableCollection(); - this.WhenAnyValue(x => x.SearchPluginInput).Throttle(TimeSpan.FromMilliseconds(300)).Subscribe(SearchPlugins); + this.WhenAnyValue(x => x.SearchPluginInput).Throttle(TimeSpan.FromMilliseconds(100)).Subscribe(SearchPlugins); this.WhenActivated((CompositeDisposable _) => GetPluginInstances()); } @@ -48,7 +48,10 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels set => this.RaiseAndSetIfChanged(ref _searchPluginInput, value); } - public void OpenUrl(string url) => Utilities.OpenUrl(url); + public void OpenUrl(string url) + { + Utilities.OpenUrl(url); + } public async Task ImportPlugin() { @@ -66,7 +69,7 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels // Enable it via the VM to enable the prerequisite dialog PluginSettingsViewModel pluginViewModel = Plugins.FirstOrDefault(i => i.Plugin == plugin); - if (pluginViewModel is { IsEnabled: false }) + if (pluginViewModel is {IsEnabled: false}) pluginViewModel.IsEnabled = true; _notificationService.CreateNotification() diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/PluginsTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/PluginsTabView.axaml index cba39cedb..b3027c1b9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/PluginsTabView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/PluginsTabView.axaml @@ -2,7 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.PluginsTabView"> - - + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Styles/Button.axaml b/src/Artemis.UI.Avalonia/Styles/Button.axaml index e1856b64e..4afc0526b 100644 --- a/src/Artemis.UI.Avalonia/Styles/Button.axaml +++ b/src/Artemis.UI.Avalonia/Styles/Button.axaml @@ -49,6 +49,11 @@ + + From b963aa0909c006b5693d3552ab674bf80755aeb9 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 13 Nov 2021 00:23:09 +0100 Subject: [PATCH 068/270] Window Service - Always provide current window as parent Plugins - Allow self-binding of unregistered services Plugins UI - Style settings window --- src/Artemis.Core/Ninject/CoreModule.cs | 3 +- .../Services/PluginManagementService.cs | 4 ++ .../Artemis.UI.Avalonia.Shared.csproj | 1 + .../Artemis.UI.Avalonia.Shared.xml | 9 ++- .../Plugins/PluginConfigurationViewModel.cs | 16 ++++-- .../Services/WindowService/WindowService.cs | 10 ++-- src/Artemis.UI.Avalonia/App.axaml.cs | 2 + .../Artemis.UI.Avalonia.csproj | 1 + .../Screens/Device/DevicePropertiesView.axaml | 55 ++++++++++--------- .../Views/PluginSettingsWindowView.axaml | 12 +++- .../Views/PluginSettingsWindowView.axaml.cs | 19 ++++++- .../Screens/Root/Views/RootView.axaml | 2 +- src/Artemis.UI.Avalonia/ViewLocator.cs | 5 +- 13 files changed, 92 insertions(+), 47 deletions(-) diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs index 7168a7ea5..5c572b62c 100644 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ b/src/Artemis.Core/Ninject/CoreModule.cs @@ -1,5 +1,4 @@ -using System.IO; -using Artemis.Core.Services; +using Artemis.Core.Services; using Artemis.Storage; using Artemis.Storage.Migrations.Interfaces; using Artemis.Storage.Repositories.Interfaces; diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 4fc31831c..9524f8a00 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -16,6 +16,7 @@ using McMaster.NETCore.Plugins; using Ninject; using Ninject.Extensions.ChildKernel; using Ninject.Parameters; +using Ninject.Planning.Bindings.Resolvers; using RGB.NET.Core; using Serilog; @@ -410,6 +411,9 @@ namespace Artemis.Core.Services // Create the Ninject child kernel and load the module plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin)); + // The kernel used by Core is unforgiving about missing bindings, no need to be so hard on plugin devs + plugin.Kernel.Components.Add(); + OnPluginEnabling(new PluginEventArgs(plugin)); plugin.SetEnabled(true); diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj index f2e68cafb..a13b34d2c 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj @@ -5,6 +5,7 @@ enable + bin\ C:\Repos\Artemis\src\Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.xml diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index e15cb64cc..569228685 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -282,9 +282,14 @@ Gets the plugin this configuration view model is associated with - + - A command that closes the window + Closes the window hosting the view model + + + + + Occurs when the the window hosting the view model should close diff --git a/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs b/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs index 008d8ccb7..194ab7eec 100644 --- a/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs +++ b/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs @@ -1,6 +1,5 @@ -using System.Reactive; +using System; using Artemis.Core; -using ReactiveUI; namespace Artemis.UI.Avalonia.Shared { @@ -16,7 +15,6 @@ namespace Artemis.UI.Avalonia.Shared protected PluginConfigurationViewModel(Plugin plugin) { Plugin = plugin; - Close = ReactiveCommand.Create(() => { }); } /// @@ -25,8 +23,16 @@ namespace Artemis.UI.Avalonia.Shared public Plugin Plugin { get; } /// - /// A command that closes the window + /// Closes the window hosting the view model /// - public ReactiveCommand Close { get; } + public void Close() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Occurs when the the window hosting the view model should close + /// + public event EventHandler? CloseRequested; } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs index 15959fe96..e11416551 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs @@ -33,6 +33,8 @@ namespace Artemis.UI.Avalonia.Shared.Services public void ShowWindow(object viewModel) { + Window parent = GetCurrentWindow(); + string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); Type? type = viewModel.GetType().Assembly.GetType(name); @@ -48,7 +50,7 @@ namespace Artemis.UI.Avalonia.Shared.Services Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; - window.Show(); + window.Show(parent); } public async Task ShowDialogAsync(params (string name, object value)[] parameters) where TViewModel : DialogViewModelBase @@ -72,10 +74,7 @@ namespace Artemis.UI.Avalonia.Shared.Services public async Task ShowDialogAsync(DialogViewModelBase viewModel) { - if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic) - { - throw new ArtemisSharedUIException("Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime."); - } + Window parent = GetCurrentWindow(); string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); Type? type = viewModel.GetType().Assembly.GetType(name); @@ -92,7 +91,6 @@ namespace Artemis.UI.Avalonia.Shared.Services Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; - Window parent = classic.Windows.FirstOrDefault(w => w.IsActive) ?? classic.MainWindow; return await window.ShowDialog(parent); } diff --git a/src/Artemis.UI.Avalonia/App.axaml.cs b/src/Artemis.UI.Avalonia/App.axaml.cs index a299455b3..ff5b99efb 100644 --- a/src/Artemis.UI.Avalonia/App.axaml.cs +++ b/src/Artemis.UI.Avalonia/App.axaml.cs @@ -42,6 +42,8 @@ namespace Artemis.UI.Avalonia private void InitializeNinject() { _kernel = new StandardKernel(); + _kernel.Settings.InjectNonPublic = true; + _kernel.Load(); _kernel.Load(); _kernel.Load(); diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj index 427d243fd..9b363bcd7 100644 --- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj @@ -3,6 +3,7 @@ WinExe net5.0 enable + bin\ diff --git a/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml index 78b19a2da..fbb52cff7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/DevicePropertiesView.axaml @@ -8,10 +8,13 @@ Title="Artemis | Device Properties" Width="1250" Height="900" - ExtendClientAreaToDecorationsHint="True" - Padding="0 32 0 0"> - - + WindowStartupLocation="CenterOwner" + ExtendClientAreaToDecorationsHint="True"> + + + + + @@ -32,10 +35,10 @@ ShowColors="True" Margin="20" /> - @@ -43,23 +46,25 @@ - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml index a8cc7d65a..2676fac81 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml @@ -4,6 +4,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginSettingsWindowView" - Title="{Binding DisplayName}"> - + Title="{Binding DisplayName}" + ExtendClientAreaToDecorationsHint="True" + Width="800" + Height="800" + WindowStartupLocation="CenterOwner"> + + + + + diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs index 0b5b3e2a3..3c8b8166f 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs @@ -1,7 +1,7 @@ using System; +using System.Reactive.Disposables; using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Avalonia; -using Avalonia.Controls.Mixins; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using ReactiveUI; @@ -17,7 +17,22 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.Views this.AttachDevTools(); #endif - this.WhenActivated(disposables => { ViewModel!.ConfigurationViewModel.Close.Subscribe(_ => Close()).DisposeWith(disposables); }); + this.WhenActivated(disposables => + { + ViewModel!.ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested; + Disposable.Create(HandleDeactivation).DisposeWith(disposables); + } + ); + } + + private void HandleDeactivation() + { + ViewModel!.ConfigurationViewModel.CloseRequested -= ConfigurationViewModelOnCloseRequested; + } + + private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e) + { + Close(); } private void InitializeComponent() diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index 50de48c2e..7b35385ee 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -13,7 +13,7 @@ - + diff --git a/src/Artemis.UI.Avalonia/ViewLocator.cs b/src/Artemis.UI.Avalonia/ViewLocator.cs index fde4e39e8..be19a2360 100644 --- a/src/Artemis.UI.Avalonia/ViewLocator.cs +++ b/src/Artemis.UI.Avalonia/ViewLocator.cs @@ -11,8 +11,9 @@ namespace Artemis.UI.Avalonia public IControl Build(object data) { - string name = data.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); - Type? type = Type.GetType(name); + Type dataType = data.GetType(); + string name = dataType.FullName!.Split('`')[0].Replace("ViewModel", "View"); + Type? type = dataType.Assembly.GetType(name); if (type != null) return (Control) Activator.CreateInstance(type)!; From 63eb0ca9b3a274c4496b027deb57b0e481543422 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 13 Nov 2021 17:46:41 +0100 Subject: [PATCH 069/270] UI - Implemented exception dialog UI - Fixed notification opacity UI - Added position control to notifications --- .../Artemis.UI.Avalonia.Shared.xml | 26 +++++----- .../Events/DialogClosedEventArgs.cs | 14 ++++++ .../Services/Builders/NotificationBuilder.cs | 23 +++++++-- .../WindowService/ExceptionDialogView.axaml | 47 +++++++++++++++++-- .../ExceptionDialogView.axaml.cs | 4 +- .../WindowService/ExceptionDialogViewModel.cs | 31 ++++++++++-- .../Services/WindowService/WindowService.cs | 28 +++++++---- .../Styles/InfoBar.axaml | 6 +-- .../ViewModelBase.cs | 39 ++++++++------- .../Artemis.UI.Avalonia.csproj.DotSettings | 2 + ...uginPrerequisitesInstallDialogViewModel.cs | 2 +- ...inPrerequisitesUninstallDialogViewModel.cs | 4 +- .../ViewModels/PluginSettingsViewModel.cs | 22 +++++---- .../Plugins/Views/PluginFeatureView.axaml | 4 +- .../Views/PluginSettingsWindowView.axaml.cs | 19 +++----- 15 files changed, 187 insertions(+), 84 deletions(-) create mode 100644 src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs create mode 100644 src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 569228685..96cbdbc6e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -455,17 +455,17 @@ - Represents the base class for Artemis view models + Represents the base class for Artemis view models - Gets or sets the display name of the view model + Gets or sets the display name of the view model - Represents the base class for Artemis view models that are interested in the activated event + Represents the base class for Artemis view models that are interested in the activated event @@ -473,11 +473,11 @@ - Releases the unmanaged resources used by the object and optionally releases the managed resources. + Releases the unmanaged resources used by the object and optionally releases the managed resources. - to release both managed and unmanaged resources; - to release only unmanaged resources. + to release both managed and unmanaged resources; + to release only unmanaged resources. @@ -488,20 +488,18 @@ - Represents the base class for Artemis view models used to drive dialogs + Represents the base class for Artemis view models used to drive dialogs - - - - + - Closes the dialog with a given result + Closes the dialog with the given + The result of the dialog - + - Closes the dialog without a result + Closes the dialog without a result diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs b/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs new file mode 100644 index 000000000..0a5aa0755 --- /dev/null +++ b/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Artemis.UI.Avalonia.Shared.Events +{ + internal class DialogClosedEventArgs : EventArgs + { + public TResult Result { get; } + + public DialogClosedEventArgs(TResult result) + { + Result = result; + } + } +} diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs b/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs index 7cd562f7c..ebfbddbff 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Layout; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using ReactiveUI; @@ -17,7 +18,12 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders public NotificationBuilder(Window parent) { _parent = parent; - _infoBar = new InfoBar {Classes = Classes.Parse("notification-info-bar")}; + _infoBar = new InfoBar + { + Classes = Classes.Parse("notification-info-bar"), + VerticalAlignment = VerticalAlignment.Bottom, + HorizontalAlignment = HorizontalAlignment.Right + }; } public NotificationBuilder WithTitle(string? title) @@ -38,6 +44,17 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders return this; } + public NotificationBuilder WithVerticalPosition(VerticalAlignment position) + { + _infoBar.VerticalAlignment = position; + return this; + } + + public NotificationBuilder WithHorizontalPosition(HorizontalAlignment position) + { + _infoBar.HorizontalAlignment = position; + return this; + } /// /// Add a filter to the dialog @@ -105,8 +122,8 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders public IControl Build() { - return _action != null - ? new Button {Content = _text, Command = ReactiveCommand.Create(() => _action)} + return _action != null + ? new Button {Content = _text, Command = ReactiveCommand.Create(() => _action)} : new Button {Content = _text}; } } diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml index 0fc655a38..8211265bb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml @@ -2,8 +2,47 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Avalonia.Shared.Services.ExceptionDialogView" - Title="ExceptionDialogView"> - Eh you got an exception but I didn't write the viewer yet :( - + Title="{Binding Title}" + ExtendClientAreaToDecorationsHint="True" + Width="800" + Height="800" + WindowStartupLocation="CenterOwner"> + + + + + + + + Awww :( + + It looks like Artemis ran into an unhandled exception. If this keeps happening feel free to hit us up on Discord. + + + + + + + + + When reporting errors please don't take a screenshot of the error, instead copy the text, thanks! + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs index 773c3f6d7..9af31290c 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs @@ -4,7 +4,7 @@ using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Shared.Services { - public partial class ExceptionDialogView : ReactiveWindow + internal class ExceptionDialogView : ReactiveWindow { public ExceptionDialogView() { @@ -19,4 +19,4 @@ namespace Artemis.UI.Avalonia.Shared.Services AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs index a43a2c1f4..66fb9bdeb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs @@ -1,12 +1,35 @@ using System; +using System.Threading.Tasks; +using Artemis.UI.Avalonia.Shared.Services.Builders; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia; +using Avalonia.Layout; namespace Artemis.UI.Avalonia.Shared.Services { - public class ExceptionDialogViewModel : DialogViewModelBase + internal class ExceptionDialogViewModel : DialogViewModelBase { - public ExceptionDialogViewModel(string title, Exception exception) + private readonly INotificationService _notificationService; + + public ExceptionDialogViewModel(string title, Exception exception, INotificationService notificationService) { - + _notificationService = notificationService; + + Title = $"Artemis | {title}"; + Exception = exception; + } + + public string Title { get; } + public Exception Exception { get; } + + public async Task CopyException() + { + await Application.Current.Clipboard.SetTextAsync(Exception.ToString()); + _notificationService.CreateNotification() + .WithMessage("Copied stack trace to clipboard.") + .WithSeverity(NotificationSeverity.Success) + .WithHorizontalPosition(HorizontalAlignment.Center) + .Show(); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs index e11416551..9cb66b8a7 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.UI.Avalonia.Shared.Exceptions; using Artemis.UI.Avalonia.Shared.Services.Builders; @@ -7,9 +8,11 @@ using Artemis.UI.Avalonia.Shared.Services.Interfaces; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Ninject; using Ninject.Parameters; +using ReactiveUI; namespace Artemis.UI.Avalonia.Shared.Services { @@ -91,25 +94,30 @@ namespace Artemis.UI.Avalonia.Shared.Services Window window = (Window) Activator.CreateInstance(type)!; window.DataContext = viewModel; + viewModel.CloseRequested += (_, args) => window.Close(args.Result); + viewModel.CancelRequested += (_, _) => window.Close(); + return await window.ShowDialog(parent); } public void ShowExceptionDialog(string title, Exception exception) { if (_exceptionDialogOpen) - { return; - } - try + _exceptionDialogOpen = true; + // Fire and forget the dialog + Dispatcher.UIThread.InvokeAsync(async () => { - _exceptionDialogOpen = true; - ShowDialogAsync(new ExceptionDialogViewModel(title, exception)).GetAwaiter().GetResult(); - } - finally - { - _exceptionDialogOpen = false; - } + try + { + await ShowDialogAsync(new ExceptionDialogViewModel(title, exception, _kernel.Get())); + } + finally + { + _exceptionDialogOpen = false; + } + }); } public ContentDialogBuilder CreateContentDialog() diff --git a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml b/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml index e58b3b634..c3be2accd 100644 --- a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml +++ b/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml @@ -13,9 +13,6 @@ - - - @@ -25,4 +22,7 @@ + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs b/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs index c0a04abda..1bb568963 100644 --- a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs +++ b/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs @@ -1,19 +1,19 @@ using System; -using System.Reactive; using System.Reactive.Disposables; +using Artemis.UI.Avalonia.Shared.Events; using ReactiveUI; namespace Artemis.UI.Avalonia.Shared { /// - /// Represents the base class for Artemis view models + /// Represents the base class for Artemis view models /// public abstract class ViewModelBase : ReactiveObject { private string? _displayName; /// - /// Gets or sets the display name of the view model + /// Gets or sets the display name of the view model /// public string? DisplayName { @@ -23,7 +23,7 @@ namespace Artemis.UI.Avalonia.Shared } /// - /// Represents the base class for Artemis view models that are interested in the activated event + /// Represents the base class for Artemis view models that are interested in the activated event /// public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable { @@ -34,11 +34,11 @@ namespace Artemis.UI.Avalonia.Shared } /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { @@ -56,25 +56,28 @@ namespace Artemis.UI.Avalonia.Shared } /// - /// Represents the base class for Artemis view models used to drive dialogs + /// Represents the base class for Artemis view models used to drive dialogs /// public abstract class DialogViewModelBase : ActivatableViewModelBase { - /// - protected DialogViewModelBase() + /// + /// Closes the dialog with the given + /// + /// The result of the dialog + public void Close(TResult result) { - Close = ReactiveCommand.Create(t => t); - Cancel = ReactiveCommand.Create(() => { }); + CloseRequested?.Invoke(this, new DialogClosedEventArgs(result)); } /// - /// Closes the dialog with a given result + /// Closes the dialog without a result /// - public ReactiveCommand Close { get; } + public void Cancel() + { + CancelRequested?.Invoke(this, EventArgs.Empty); + } - /// - /// Closes the dialog without a result - /// - public ReactiveCommand Cancel { get; } + internal event EventHandler>? CloseRequested; + internal event EventHandler? CancelRequested; } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings new file mode 100644 index 000000000..452c5acb6 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs index 2e47658c4..af0818afd 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs @@ -118,7 +118,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels public void Accept() { - Close.Execute(true); + Close(true); } public static async Task Show(IWindowService windowService, List subjects) diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs index a14426227..238cd309e 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs @@ -105,7 +105,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels // This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case) // but at least give some feedback - Close.Execute(false); + Close(false); await _windowService.CreateContentDialog() .WithTitle("Plugin prerequisites") .WithContent("The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.") @@ -126,7 +126,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels public void Accept() { - Close.Execute(true); + Close(true); } public static async Task Show(IWindowService windowService, List subjects, string cancelLabel = "Cancel") diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs index 3dbad61ee..9ae740a19 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs @@ -12,6 +12,7 @@ using Artemis.UI.Avalonia.Exceptions; using Artemis.UI.Avalonia.Ninject.Factories; using Artemis.UI.Avalonia.Shared; using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; using Ninject; using ReactiveUI; @@ -53,9 +54,13 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled; OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null)); + InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites)); + RemovePrerequisites = ReactiveCommand.CreateFromTask(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites)); } public ReactiveCommand OpenSettings { get; } + public ReactiveCommand InstallPrerequisites { get; } + public ReactiveCommand RemovePrerequisites { get; } public ObservableCollection PluginFeatures { get; } @@ -72,7 +77,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels } public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; - + public bool IsEnabled { get => Plugin.IsEnabled; @@ -137,7 +142,8 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels { bool wasEnabled = IsEnabled; - _pluginManagementService.UnloadPlugin(Plugin); + await Task.Run(() => _pluginManagementService.UnloadPlugin(Plugin)); + PluginFeatures.Clear(); Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory); @@ -150,7 +156,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels _notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show(); } - public async Task InstallPrerequisites() + public async Task ExecuteInstallPrerequisites() { List subjects = new() {Plugin.Info}; subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); @@ -159,7 +165,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); } - public async Task RemovePrerequisites(bool forPluginRemoval = false) + public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false) { List subjects = new() {Plugin.Info}; subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); @@ -200,7 +206,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels List subjects = new() {Plugin.Info}; subjects.AddRange(Plugin.Features); if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) - await RemovePrerequisites(true); + await ExecuteRemovePrerequisites(true); try { @@ -284,7 +290,7 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels } } - await Task.Run(() => + await Task.Run(async () => { try { @@ -292,10 +298,10 @@ namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels } catch (Exception e) { - _notificationService.CreateNotification() + await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() .WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}") .HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder)) - .Show(); + .Show()); } finally { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml index 9100a1af1..4f990e857 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml @@ -27,12 +27,10 @@ Icon="{Binding FeatureInfo.ResolvedIcon}" Width="20" Height="20" - IsVisible="{Binding LoadException, Converter={x:Static ObjectConverters.IsNull}}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml.cs new file mode 100644 index 000000000..50bb1b25a --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.Device.Views +{ + public partial class DeviceSettingsView : UserControl + { + public DeviceSettingsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml index 2ff5e265b..ddf9602d9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml @@ -12,7 +12,7 @@ - + diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs index 3423b8c09..39fdafccc 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs @@ -1,12 +1,87 @@ -using Artemis.UI.Avalonia.Shared; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Screens.Device.ViewModels; +using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels { public class DevicesTabViewModel : ActivatableViewModelBase { - public DevicesTabViewModel() + private readonly IDeviceVmFactory _deviceVmFactory; + private readonly IRgbService _rgbService; + private readonly IWindowService _windowService; + private bool _confirmedDisable; + + public DevicesTabViewModel(IRgbService rgbService, IWindowService windowService, IDeviceVmFactory deviceVmFactory) { DisplayName = "Devices"; + + _rgbService = rgbService; + _windowService = windowService; + _deviceVmFactory = deviceVmFactory; + + Devices = new ObservableCollection(); + this.WhenActivated(disposables => + { + GetDevices(); + + Observable.FromEventPattern(x => rgbService.DeviceAdded += x, x => rgbService.DeviceAdded -= x) + .Subscribe(d => AddDevice(d.EventArgs.Device)) + .DisposeWith(disposables); + Observable.FromEventPattern(x => rgbService.DeviceRemoved += x, x => rgbService.DeviceRemoved -= x) + .Subscribe(d => RemoveDevice(d.EventArgs.Device)) + .DisposeWith(disposables); + }); + } + + private void GetDevices() + { + Devices.Clear(); + Dispatcher.UIThread.InvokeAsync(() => + { + Devices.AddRange(_rgbService.Devices.Select(d => _deviceVmFactory.DeviceSettingsViewModel(d, this))); + }, DispatcherPriority.Background); + } + + public ObservableCollection Devices { get; } + + public async Task ShowDeviceDisableDialog() + { + if (_confirmedDisable) + return true; + + bool confirmed = await _windowService.ShowConfirmContentDialog( + "Disabling device", + "Disabling a device will cause it to stop updating. " + + "\r\nSome SDKs will even go back to using manufacturer lighting (Artemis restart may be required)." + ); + if (confirmed) + _confirmedDisable = true; + + return confirmed; + } + + private void AddDevice(ArtemisDevice device) + { + Devices.Add(_deviceVmFactory.DeviceSettingsViewModel(device, this)); + } + + private void RemoveDevice(ArtemisDevice device) + { + DeviceSettingsViewModel? viewModel = Devices.FirstOrDefault(i => i.Device == device); + if (viewModel != null) + Devices.Remove(viewModel); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs index bbf898563..6c2928bfe 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs @@ -13,6 +13,7 @@ using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; using Artemis.UI.Avalonia.Shared; using Artemis.UI.Avalonia.Shared.Services.Builders; using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; using ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels @@ -82,12 +83,16 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels public void GetPluginInstances() { - _instances = _pluginManagementService.GetAllPlugins() - .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) - .OrderBy(i => i.Plugin.Info.Name) - .ToList(); + Plugins.Clear(); + Dispatcher.UIThread.InvokeAsync(() => + { + _instances = _pluginManagementService.GetAllPlugins() + .Select(p => _settingsVmFactory.CreatePluginSettingsViewModel(p)) + .OrderBy(i => i.Plugin.Info.Name) + .ToList(); - SearchPlugins(SearchPluginInput); + SearchPlugins(SearchPluginInput); + }, DispatcherPriority.Background); } private void SearchPlugins(string? searchPluginInput) diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml index 22fdc9002..fe66685ba 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml @@ -4,5 +4,22 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.DevicesTabView"> - Welcome to Avalonia! - + + Device management + + Below you view and manage the devices that were detected by Artemis. + + + Disabling a device will cause it to stop updating. Some SDKs will even go back to using manufacturer lighting (Artemis restart may be required). + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs index 9efeb4f18..84e9a9ed7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs @@ -1,9 +1,10 @@ -using Avalonia.Controls; +using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views { - public partial class DevicesTabView : UserControl + public class DevicesTabView : ReactiveUserControl { public DevicesTabView() { @@ -15,4 +16,4 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file From 291d71edcb54a91c990724b0d544c4200dfdcc89 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 15 Nov 2021 22:37:02 +0100 Subject: [PATCH 071/270] Device settings - Don't add/remove enabled/disabled devices --- .../Controls/DeviceVisualizer.cs | 62 +++++++++---------- .../ViewModels/DeviceSettingsViewModel.cs | 3 +- .../Device/Views/DeviceSettingsView.axaml | 4 +- .../Tabs/ViewModels/DevicesTabViewModel.cs | 8 +++ 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs index 543d83156..9c6a592e1 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs @@ -92,6 +92,13 @@ namespace Artemis.UI.Avalonia.Shared.Controls { _deviceImage?.Dispose(); _deviceImage = null; + + if (Device != null) + { + Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; + Device.DeviceUpdated -= DeviceUpdated; + } + base.OnDetachedFromVisualTree(e); } @@ -109,12 +116,6 @@ namespace Artemis.UI.Avalonia.Shared.Controls InvalidateVisual(); } - private void UpdateTransform() - { - InvalidateVisual(); - InvalidateMeasure(); - } - private Rect MeasureDevice() { if (Device == null) @@ -150,12 +151,12 @@ namespace Artemis.UI.Avalonia.Shared.Controls private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) { - Dispatcher.UIThread.Post(SetupForDevice); + Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background); } private void DeviceUpdated(object? sender, EventArgs e) { - Dispatcher.UIThread.Post(SetupForDevice); + Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background); } #region Properties @@ -239,21 +240,20 @@ namespace Artemis.UI.Avalonia.Shared.Controls _highlightedLeds = new List(); _dimmedLeds = new List(); - if (Device == null) - return; - if (_oldDevice != null) { - Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; - Device.DeviceUpdated -= DeviceUpdated; + _oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged; + _oldDevice.DeviceUpdated -= DeviceUpdated; } _oldDevice = Device; + if (Device == null) + return; + _deviceBounds = MeasureDevice(); Device.RgbDevice.PropertyChanged += DevicePropertyChanged; Device.DeviceUpdated += DeviceUpdated; - UpdateTransform(); // Create all the LEDs foreach (ArtemisLed artemisLed in Device.Leds) @@ -263,27 +263,27 @@ namespace Artemis.UI.Avalonia.Shared.Controls ArtemisDevice? device = Device; Task.Run(() => { - if (device.Layout?.Image != null && File.Exists(device.Layout.Image.LocalPath)) + if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) + return; + + try { - try - { - // Create a bitmap that'll be used to render the device and LED images just once - RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.Size.Width * 4, (int) device.RgbDevice.Size.Height * 4)); + // Create a bitmap that'll be used to render the device and LED images just once + RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.Size.Width * 4, (int) device.RgbDevice.Size.Height * 4)); - using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this)); - using Bitmap bitmap = new(device.Layout.Image.LocalPath); - context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality); - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.DrawBitmap(context); + using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this)); + using Bitmap bitmap = new(device.Layout.Image.LocalPath); + context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality); + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.DrawBitmap(context); - _deviceImage = renderTargetBitmap; + _deviceImage = renderTargetBitmap; - Dispatcher.UIThread.Post(InvalidateMeasure); - } - catch - { - // ignored - } + Dispatcher.UIThread.Post(InvalidateMeasure); + } + catch + { + // ignored } }); } diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs index 12fbe647a..a5347253c 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs @@ -5,6 +5,7 @@ using Artemis.UI.Avalonia.Ninject.Factories; using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; using Artemis.UI.Avalonia.Shared; using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Threading; using Humanizer; using ReactiveUI; using RGB.NET.Core; @@ -45,7 +46,7 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels public bool IsDeviceEnabled { get => Device.IsEnabled; - set => Task.Run(() => UpdateIsDeviceEnabled(value)); + set => Dispatcher.UIThread.InvokeAsync(async () => await UpdateIsDeviceEnabled(value)); } public void IdentifyDevice() diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml index 8bbc47641..338de63ce 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml @@ -11,9 +11,9 @@ - + - + diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs index 39fdafccc..4f53e7142 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs @@ -74,11 +74,19 @@ namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels private void AddDevice(ArtemisDevice device) { + // If the device was only enabled, don't add it + if (Devices.Any(d => d.Device == device)) + return; + Devices.Add(_deviceVmFactory.DeviceSettingsViewModel(device, this)); } private void RemoveDevice(ArtemisDevice device) { + // If the device was only disabled don't remove it + if (_rgbService.Devices.Contains(device)) + return; + DeviceSettingsViewModel? viewModel = Devices.FirstOrDefault(i => i.Device == device); if (viewModel != null) Devices.Remove(viewModel); From 9e6976c5e79956fd14b430f8f7cfe296c39fc1b3 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 15 Nov 2021 23:50:16 +0100 Subject: [PATCH 072/270] Content dialog builder - Fix WithViewModel Device visualizer - Fix possible measure crash Device settings - Added input detection VM --- .../Controls/DeviceVisualizer.cs | 5 +- .../Services/Builders/ContentDialogBuilder.cs | 6 +-- .../ViewModels/DeviceDetectInputViewModel.cs | 51 +++++++++++++++++-- .../ViewModels/DeviceSettingsViewModel.cs | 45 ++++++++++++---- .../Device/Views/DeviceDetectInputView.axaml | 27 ++++++++-- .../Views/DeviceDetectInputView.axaml.cs | 8 +-- .../Settings/Tabs/Views/DevicesTabView.axaml | 2 +- 7 files changed, 119 insertions(+), 25 deletions(-) diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs index 9c6a592e1..af925a65d 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs @@ -263,7 +263,7 @@ namespace Artemis.UI.Avalonia.Shared.Controls ArtemisDevice? device = Device; Task.Run(() => { - if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) + if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) return; try @@ -291,6 +291,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls /// protected override Size MeasureOverride(Size availableSize) { + if (_deviceBounds.Width <= 0 || _deviceBounds.Height <= 0) + return new Size(0, 0); + double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width; double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height; double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height); diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs index e082e189e..9cf160b09 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs @@ -22,7 +22,7 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders _parent = parent; _contentDialog = new ContentDialog { - CloseButtonText = "CLose" + CloseButtonText = "Close" }; } @@ -76,11 +76,11 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders return this; } - public ContentDialogBuilder WithViewModel(out T viewModel, params (string name, object value)[] parameters) + public ContentDialogBuilder WithViewModel(out T viewModel, params (string name, object value)[] parameters) where T : ViewModelBase { IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); viewModel = _kernel.Get(paramsArray); - _contentDialog.Content = _kernel.Get(); + _contentDialog.Content = viewModel; return this; } diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs index 636532a0f..8eb1de492 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs @@ -1,15 +1,60 @@ -using Artemis.Core; +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Avalonia.Shared.Services.Builders; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using ReactiveUI; +using RGB.NET.Core; namespace Artemis.UI.Avalonia.Screens.Device.ViewModels { - public class DeviceDetectInputViewModel + public class DeviceDetectInputViewModel : ActivatableViewModelBase { - public DeviceDetectInputViewModel(ArtemisDevice device) + private readonly IInputService _inputService; + private readonly INotificationService _notificationService; + private readonly IRgbService _rgbService; + private readonly ListLedGroup _ledGroup; + + public DeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, INotificationService notificationService, IRgbService rgbService) { + _inputService = inputService; + _notificationService = notificationService; + _rgbService = rgbService; + Device = device; + + // Create a LED group way at the top + _ledGroup = new ListLedGroup(_rgbService.Surface, Device.Leds.Select(l => l.RgbLed)) + { + Brush = new SolidColorBrush(new Color(255, 255, 0)), + ZIndex = 999 + }; + + this.WhenActivated(disposables => + { + Observable.FromEventPattern(x => _inputService.DeviceIdentified += x, x => _inputService.DeviceIdentified -= x) + .Subscribe(_ => InputServiceOnDeviceIdentified()) + .DisposeWith(disposables); + + Disposable.Create(() => _ledGroup.Detach()).DisposeWith(disposables); + }); } public ArtemisDevice Device { get; } + public bool IsMouse => Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse; + public bool MadeChanges { get; set; } + + private void InputServiceOnDeviceIdentified() + { + _notificationService.CreateNotification() + .WithMessage($"{Device.RgbDevice.DeviceInfo.DeviceName} identified 😁") + .WithSeverity(NotificationSeverity.Success) + .Show(); + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs index a5347253c..f724a068f 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Reactive; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Avalonia.Ninject.Factories; @@ -19,6 +20,7 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels private readonly IDeviceVmFactory _deviceVmFactory; private readonly IRgbService _rgbService; private readonly IWindowService _windowService; + private bool _togglingDevice; public DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel, IDeviceService deviceService, IWindowService windowService, IDeviceVmFactory deviceVmFactory, IRgbService rgbService) @@ -33,6 +35,8 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels Type = Device.DeviceType.ToString().Humanize(); Name = Device.RgbDevice.DeviceInfo.Model; Manufacturer = Device.RgbDevice.DeviceInfo.Manufacturer; + + DetectInput = ReactiveCommand.CreateFromTask(ExecuteDetectInput, this.WhenAnyValue(vm => vm.CanDetectInput)); } public ArtemisDevice Device { get; } @@ -42,6 +46,7 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels public string Manufacturer { get; } public bool CanDetectInput => Device.DeviceType is RGBDeviceType.Keyboard or RGBDeviceType.Mouse; + public ReactiveCommand DetectInput { get; } public bool IsDeviceEnabled { @@ -49,6 +54,12 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels set => Dispatcher.UIThread.InvokeAsync(async () => await UpdateIsDeviceEnabled(value)); } + public bool TogglingDevice + { + get => _togglingDevice; + set => this.RaiseAndSetIfChanged(ref _togglingDevice, value); + } + public void IdentifyDevice() { _deviceService.IdentifyDevice(Device); @@ -59,13 +70,15 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels Utilities.OpenFolder(Device.DeviceProvider.Plugin.Directory.FullName); } - public async Task DetectInput() + private async Task ExecuteDetectInput() { if (!CanDetectInput) return; await _windowService.CreateContentDialog() + .WithTitle($"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input") .WithViewModel(out var viewModel, ("device", Device)) + .WithCloseButtonText("Cancel") .ShowAsync(); if (viewModel.MadeChanges) @@ -79,16 +92,28 @@ namespace Artemis.UI.Avalonia.Screens.Device.ViewModels private async Task UpdateIsDeviceEnabled(bool value) { - if (!value) - value = !await _devicesTabViewModel.ShowDeviceDisableDialog(); + if (TogglingDevice) + return; - if (value) - _rgbService.EnableDevice(Device); - else - _rgbService.DisableDevice(Device); + try + { + TogglingDevice = true; - this.RaisePropertyChanged(nameof(IsDeviceEnabled)); - SaveDevice(); + if (!value) + value = !await _devicesTabViewModel.ShowDeviceDisableDialog(); + + if (value) + _rgbService.EnableDevice(Device); + else + _rgbService.DisableDevice(Device); + + this.RaisePropertyChanged(nameof(IsDeviceEnabled)); + SaveDevice(); + } + finally + { + TogglingDevice = false; + } } private void SaveDevice() diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml index 71c6948f7..113d6d9b8 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml @@ -2,7 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="1050" x:Class="Artemis.UI.Avalonia.Screens.Device.Views.DeviceDetectInputView"> - Welcome to Avalonia! - + + + + Press a button/key on your device that is currently showing a yellow color. + + + + + + + This will teach Artemis to associate button/key presses with this specific device. + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs index ed56b2222..cd60e9293 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs @@ -1,10 +1,10 @@ -using Avalonia; -using Avalonia.Controls; +using Artemis.UI.Avalonia.Screens.Device.ViewModels; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Avalonia.Screens.Device.Views { - public partial class DeviceDetectInputView : UserControl + public class DeviceDetectInputView : ReactiveUserControl { public DeviceDetectInputView() { @@ -16,4 +16,4 @@ namespace Artemis.UI.Avalonia.Screens.Device.Views AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml index fe66685ba..1d97437af 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml @@ -13,7 +13,7 @@ Disabling a device will cause it to stop updating. Some SDKs will even go back to using manufacturer lighting (Artemis restart may be required). - + From 741eb6d919b52bf8049f52d571e77c7ef999409f Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 15 Nov 2021 23:59:59 +0100 Subject: [PATCH 073/270] Sidebar - Reduce separator brightness --- src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml index 9210a465a..6ed5d7849 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarView.axaml @@ -43,7 +43,7 @@ Margin="10 2" Items="{Binding SidebarScreens}" SelectedItem="{Binding SelectedSidebarScreen}" /> - + From ef4657725582d96eff3875815657ad5cb9f0c7d8 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 16 Nov 2021 00:13:24 +0100 Subject: [PATCH 074/270] Device settings - Fix opening device properties --- .../Screens/Debugger/DebugView.axaml | 18 +++++------------- .../Device/Views/DeviceSettingsView.axaml | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml b/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml index 7867c29a0..1f90a6a68 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Avalonia.Screens.Debugger.DebugView" Title="Artemis | Debugger" @@ -9,18 +10,9 @@ Height="800" ExtendClientAreaToDecorationsHint="True"> - - - - - Test - - - - Test - - + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml index 338de63ce..b1b8e5ae1 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml @@ -49,7 +49,7 @@ - + From bf8bef8a20d214269109131ea2798da43d9e5095 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 17 Nov 2021 22:11:08 +0100 Subject: [PATCH 075/270] Device properties - Added info tab --- .../Providers/AvaloniaInputProvider.cs | 8 ++ .../Tabs/ViewModels/DeviceInfoTabViewModel.cs | 9 ++- .../Device/Tabs/Views/DeviceInfoTabView.axaml | 81 ++++++++++++++++++- .../Screens/Root/ViewModels/RootViewModel.cs | 5 +- .../Interfaces/IRegistrationService.cs | 12 +++ .../Services/RegistrationService.cs | 40 +++++++++ 6 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs create mode 100644 src/Artemis.UI.Avalonia/Services/Interfaces/IRegistrationService.cs create mode 100644 src/Artemis.UI.Avalonia/Services/RegistrationService.cs diff --git a/src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs b/src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs new file mode 100644 index 000000000..ca50b3734 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs @@ -0,0 +1,8 @@ +using Artemis.Core.Services; + +namespace Artemis.UI.Avalonia.Providers +{ + public class AvaloniaInputProvider : InputProvider + { + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs index 91d0d7d02..bd93e2113 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; using Avalonia; using RGB.NET.Core; @@ -8,8 +9,12 @@ namespace Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels { public class DeviceInfoTabViewModel : ActivatableViewModelBase { - public DeviceInfoTabViewModel(ArtemisDevice device) + private readonly INotificationService _notificationService; + + public DeviceInfoTabViewModel(ArtemisDevice device, INotificationService notificationService) { + _notificationService = notificationService; + Device = device; DisplayName = "Info"; @@ -24,7 +29,7 @@ namespace Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels public async Task CopyToClipboard(string content) { await Application.Current.Clipboard.SetTextAsync(content); - // ((DeviceDialogViewModel) Parent).DeviceMessageQueue.Enqueue("Copied path to clipboard."); + _notificationService.CreateNotification().WithMessage("Copied path to clipboard.").Show(); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml index 443b06da8..3a298e870 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml @@ -2,7 +2,84 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Device.Tabs.Views.DeviceInfoTabView"> - Welcome to Avalonia! - + + + + + Device name + + + + Manufacturer + + + + + Device type + + + + + Physical layout + + + + + + + + Size (1px = 1mm) + + + + Location (1px = 1mm) + + + + Rotation (degrees) + + + + + Logical layout + + + + + + + + + + Default layout file path + + + + + + + Image file path + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs index 87577cf26..7edbfc0f7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core.Services; using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Services.Interfaces; using Artemis.UI.Avalonia.Shared; using ReactiveUI; @@ -9,13 +10,15 @@ namespace Artemis.UI.Avalonia.Screens.Root.ViewModels { private readonly ICoreService _coreService; - public RootViewModel(ICoreService coreService, ISidebarVmFactory sidebarVmFactory) + public RootViewModel(ICoreService coreService, IRegistrationService registrationService, ISidebarVmFactory sidebarVmFactory) { Router = new RoutingState(); SidebarViewModel = sidebarVmFactory.SidebarViewModel(this); _coreService = coreService; _coreService.Initialize(); + + registrationService.RegisterProviders(); } public SidebarViewModel SidebarViewModel { get; } diff --git a/src/Artemis.UI.Avalonia/Services/Interfaces/IRegistrationService.cs b/src/Artemis.UI.Avalonia/Services/Interfaces/IRegistrationService.cs new file mode 100644 index 000000000..d65d09864 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Services/Interfaces/IRegistrationService.cs @@ -0,0 +1,12 @@ +namespace Artemis.UI.Avalonia.Services.Interfaces +{ + public interface IRegistrationService : IArtemisUIService + { + void RegisterBuiltInDataModelDisplays(); + void RegisterBuiltInDataModelInputs(); + void RegisterBuiltInPropertyEditors(); + void RegisterProviders(); + void RegisterControllers(); + void ApplyPreferredGraphicsContext(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Services/RegistrationService.cs b/src/Artemis.UI.Avalonia/Services/RegistrationService.cs new file mode 100644 index 000000000..1b10cf056 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Services/RegistrationService.cs @@ -0,0 +1,40 @@ +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Providers; +using Artemis.UI.Avalonia.Services.Interfaces; + +namespace Artemis.UI.Avalonia.Services +{ + public class RegistrationService : IRegistrationService + { + private readonly IInputService _inputService; + + public RegistrationService(IInputService inputService) + { + _inputService = inputService; + } + public void RegisterBuiltInDataModelDisplays() + { + } + + public void RegisterBuiltInDataModelInputs() + { + } + + public void RegisterBuiltInPropertyEditors() + { + } + + public void RegisterProviders() + { + _inputService.AddInputProvider(new AvaloniaInputProvider()); + } + + public void RegisterControllers() + { + } + + public void ApplyPreferredGraphicsContext() + { + } + } +} \ No newline at end of file From c9c8bb70787fb805549b55e39b44393517ca86d7 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 17 Nov 2021 22:56:27 +0100 Subject: [PATCH 076/270] Devices - Fix margin in device info tab --- .../Screens/Device/Tabs/Views/DeviceInfoTabView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml index 3a298e870..18d3375c6 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml @@ -5,7 +5,7 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.Device.Tabs.Views.DeviceInfoTabView"> - + From 1e36297c6199e053c0a6bc2cd6370cc8ddea5357 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 17 Nov 2021 23:10:11 +0100 Subject: [PATCH 077/270] Fix build --- .../EnumEqualsNodeCustomViewModel.cs | 6 +++--- src/Artemis.sln | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs index 65f0eb05a..b42530baf 100644 --- a/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs @@ -1,6 +1,6 @@ -using Artemis.Core; +using System; +using Artemis.Core; using Artemis.Core.Events; -using Artemis.UI.Shared; using Stylet; namespace Artemis.VisualScripting.Nodes.CustomViewModels @@ -14,7 +14,7 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels _node = node; } - public BindableCollection EnumValues { get; } = new(); + public BindableCollection<(Enum, string)> EnumValues { get; } = new(); public override void OnActivate() { diff --git a/src/Artemis.sln b/src/Artemis.sln index 7ba869e7a..10dd6db3f 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -15,10 +15,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Console", "Artem EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Avalonia", "Artemis.UI.Avalonia\Artemis.UI.Avalonia.csproj", "{035CBB38-7B9E-4375-A39C-E9A5B01F23A5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.Avalonia.Shared", "Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.csproj", "{05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Avalonia.Shared", "Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.csproj", "{05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{CF125C61-FD85-47EE-AF64-38B8F90DD50C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WPF", "WPF", "{EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Avalonia", "Avalonia", "{960CAAC5-AA73-49F5-BF2F-DF2C789DF042}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,14 +77,23 @@ Global {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|Any CPU.Build.0 = Release|Any CPU {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|x64.ActiveCfg = Release|Any CPU {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}.Release|x64.Build.0 = Release|Any CPU + {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Debug|Any CPU.ActiveCfg = Debug|x64 {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Debug|x64.ActiveCfg = Debug|x64 {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Debug|x64.Build.0 = Debug|x64 + {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Release|Any CPU.ActiveCfg = Release|x64 {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Release|x64.ActiveCfg = Release|x64 {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {46B74153-77CF-4489-BDF9-D53FDB1F7ACB} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} + {ADB357E6-151D-4D0D-87CB-68FD0BC29812} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} + {035CBB38-7B9E-4375-A39C-E9A5B01F23A5} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} + {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} + {CF125C61-FD85-47EE-AF64-38B8F90DD50C} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C203080A-4473-4CC2-844B-F552EA43D66A} EndGlobalSection From f38a9e9e5502b220b9fbdee29ad3a647657eda06 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 18 Nov 2021 00:18:13 +0100 Subject: [PATCH 078/270] Rename Avalonia projects Add Avalonia platform projects --- .../Artemis.UI.Avalonia.Shared.xml | 216 +-- .../Artemis.UI.Avalonia.csproj | 117 -- .../ViewModels/ProfileEditorViewModel.cs | 8 - .../Plugins/PluginConfigurationDialog.cs | 4 +- src/Artemis.UI/Artemis.UI.csproj.DotSettings | 5 - src/Artemis.sln | 39 +- src/Avalonia/Artemis.UI.Linux/.gitignore | 454 +++++ src/Avalonia/Artemis.UI.Linux/App.axaml | 13 + src/Avalonia/Artemis.UI.Linux/App.axaml.cs | 50 + .../Artemis.UI.Linux/Artemis.UI.Linux.csproj | 22 + .../Assets/avalonia-logo.ico | Bin .../Artemis.UI.Linux}/Program.cs | 14 +- .../Artemis.UI.Linux/packages.lock.json | 1720 +++++++++++++++++ src/Avalonia/Artemis.UI.MacOS/.gitignore | 454 +++++ src/Avalonia/Artemis.UI.MacOS/App.axaml | 13 + src/Avalonia/Artemis.UI.MacOS/App.axaml.cs | 50 + .../Artemis.UI.MacOS/Artemis.UI.MacOS.csproj | 22 + .../Artemis.UI.MacOS/Assets/avalonia-logo.ico | Bin 0 -> 176111 bytes src/Avalonia/Artemis.UI.MacOS/Program.cs | 24 + .../Artemis.UI.MacOS/packages.lock.json | 1720 +++++++++++++++++ ...emis.UI.Avalonia.Shared.csproj.DotSettings | 0 .../Artemis.UI.Avalonia.Shared.xml | 506 +++++ .../Artemis.UI.Shared.csproj} | 4 +- .../Artemis.UI.Shared.csproj.DotSettings | 3 + .../Controls/ArtemisIcon.axaml | 2 +- .../Controls/ArtemisIcon.axaml.cs | 3 +- .../Controls/DeviceVisualizer.cs | 4 +- .../Controls/DeviceVisualizerLed.cs | 2 +- .../Controls/EnumComboBox.axaml | 2 +- .../Controls/EnumComboBox.axaml.cs | 3 +- .../Controls/ProfileConfigurationIcon.axaml | 2 +- .../ProfileConfigurationIcon.axaml.cs | 2 +- .../Controls/SelectionRectangle.cs | 2 +- .../Converters/ColorToSKColorConverter.cs | 2 +- .../Converters/SKColorToColorConverter.cs | 2 +- .../Events/DataModelInputDynamicEventArgs.cs | 2 +- .../Events/DataModelInputStaticEventArgs.cs | 2 +- .../Events/DialogClosedEventArgs.cs | 2 +- .../Events/LedClickedEventArgs.cs | 2 +- .../Events/ProfileConfigurationEventArgs.cs | 2 +- .../Events/RenderProfileElementEventArgs.cs | 2 +- .../Events/SelectionRectangleEventArgs.cs | 2 +- .../Exceptions/ArtemisUIException.cs | 2 +- .../Ninject/SharedUIModule.cs | 4 +- .../Plugins/PluginConfigurationDialog.cs | 2 +- .../Plugins/PluginConfigurationViewModel.cs | 2 +- .../Services/Builders/ContentDialogBuilder.cs | 3 +- .../Builders/FileDialogFilterBuilder.cs | 2 +- .../Services/Builders/NotificationBuilder.cs | 2 +- .../Builders/OpenFileDialogBuilder.cs | 2 +- .../Builders/SaveFileDialogBuilder.cs | 2 +- .../Interfaces/IArtemisSharedUIService.cs | 2 +- .../Interfaces/INotificationService.cs | 4 +- .../Services/Interfaces/IWindowService.cs | 4 +- .../Services/NotificationService.cs | 6 +- .../WindowService/ExceptionDialogView.axaml | 2 +- .../ExceptionDialogView.axaml.cs | 2 +- .../WindowService/ExceptionDialogViewModel.cs | 6 +- .../Services/WindowService/WindowService.cs | 10 +- .../Artemis.UI.Shared}/Styles/InfoBar.axaml | 0 .../Artemis.UI.Shared}/ViewModelBase.cs | 4 +- .../Artemis.UI.Shared}/nuget.config | 0 .../Artemis.UI.Shared}/packages.lock.json | 0 src/Avalonia/Artemis.UI.Windows/.gitignore | 454 +++++ src/Avalonia/Artemis.UI.Windows/App.axaml | 31 + .../Artemis.UI.Windows}/App.axaml.cs | 8 +- .../Artemis.UI.Windows.csproj | 22 + .../Assets/avalonia-logo.ico | Bin 0 -> 176111 bytes src/Avalonia/Artemis.UI.Windows/Program.cs | 24 + .../Artemis.UI.Windows/packages.lock.json | 1720 +++++++++++++++++ .../Artemis.UI}/App.axaml | 6 +- src/Avalonia/Artemis.UI/App.axaml.cs | 54 + .../Artemis.UI.Avalonia.csproj.DotSettings | 0 src/Avalonia/Artemis.UI/Artemis.UI.csproj | 45 + .../Artemis.UI/Artemis.UI.csproj.DotSettings | 2 + .../Assets/Images/Logo/bow-black.ico | Bin .../Assets/Images/Logo/bow-white.ico | Bin .../Assets/Images/Logo/bow-white.svg | 0 .../Artemis.UI}/Assets/Images/Logo/bow.ico | Bin .../Artemis.UI}/Assets/Images/Logo/bow.svg | 0 .../Artemis.UI}/Assets/Images/home-banner.png | Bin .../Artemis.UI/Assets/avalonia-logo.ico | Bin 0 -> 176111 bytes .../ColorToSolidColorBrushConverter.cs | 2 +- .../Converters/EnumToCollectionConverter.cs | 2 +- .../Converters/LedIdToStringConverter.cs | 2 +- .../NormalizedPercentageConverter.cs | 2 +- .../Converters/UriToFileNameConverter.cs | 2 +- .../Converters/ValuesAdditionConverter.cs | 2 +- .../ArtemisGraphicsContextException.cs | 2 +- .../Exceptions/ArtemisUIException.cs | 2 +- .../BindableCollectionExtensions.cs | 2 +- .../Artemis.UI}/MainWindow.axaml | 2 +- .../Artemis.UI}/MainWindow.axaml.cs | 4 +- .../Ninject/Factories/IVMFactory.cs | 15 +- .../Artemis.UI}/Ninject/UIModule.cs | 10 +- .../Providers/AvaloniaInputProvider.cs | 2 +- .../Screens/Debugger/DebugView.axaml | 2 +- .../Screens/Debugger/DebugView.axaml.cs | 4 +- .../Screens/Debugger/DebugViewModel.cs | 14 +- .../Tabs/DataModel/DataModelDebugView.axaml | 2 +- .../DataModel/DataModelDebugView.axaml.cs | 2 +- .../Tabs/DataModel/DataModelDebugViewModel.cs | 4 +- .../Debugger/Tabs/Logs/LogsDebugView.axaml | 2 +- .../Debugger/Tabs/Logs/LogsDebugView.axaml.cs | 2 +- .../Debugger/Tabs/Logs/LogsDebugViewModel.cs | 4 +- .../Performance/PerformanceDebugView.axaml | 2 +- .../Performance/PerformanceDebugView.axaml.cs | 2 +- .../Performance/PerformanceDebugViewModel.cs | 4 +- .../Tabs/Render/RenderDebugView.axaml | 2 +- .../Tabs/Render/RenderDebugView.axaml.cs | 2 +- .../Tabs/Render/RenderDebugViewModel.cs | 4 +- .../Tabs/ViewModels/DeviceInfoTabViewModel.cs | 6 +- .../Tabs/ViewModels/DeviceLedsTabViewModel.cs | 4 +- .../DevicePropertiesTabViewModel.cs | 8 +- .../ViewModels/InputMappingsTabViewModel.cs | 6 +- .../Device/Tabs/Views/DeviceInfoTabView.axaml | 2 +- .../Tabs/Views/DeviceInfoTabView.axaml.cs | 2 +- .../Device/Tabs/Views/DeviceLedsTabView.axaml | 2 +- .../Tabs/Views/DeviceLedsTabView.axaml.cs | 2 +- .../Tabs/Views/DevicePropertiesTabView.axaml | 6 +- .../Views/DevicePropertiesTabView.axaml.cs | 4 +- .../Tabs/Views/InputMappingsTabView.axaml | 2 +- .../Tabs/Views/InputMappingsTabView.axaml.cs | 2 +- .../ViewModels/DeviceDetectInputViewModel.cs | 8 +- .../ViewModels/DevicePropertiesViewModel.cs | 6 +- .../ViewModels/DeviceSettingsViewModel.cs | 10 +- .../Device/Views/DeviceDetectInputView.axaml | 2 +- .../Views/DeviceDetectInputView.axaml.cs | 4 +- .../Device/Views/DevicePropertiesView.axaml | 4 +- .../Views/DevicePropertiesView.axaml.cs | 3 +- .../Device/Views/DeviceSettingsView.axaml | 6 +- .../Device/Views/DeviceSettingsView.axaml.cs | 3 +- .../Screens/Home/ViewModels/HomeViewModel.cs | 2 +- .../Screens/Home/Views/HomeView.axaml | 2 +- .../Screens/Home/Views/HomeView.axaml.cs | 4 +- .../Screens/MainScreenViewModel.cs | 4 +- .../ViewModels/PluginFeatureViewModel.cs | 9 +- .../PluginPrerequisiteActionViewModel.cs | 4 +- .../ViewModels/PluginPrerequisiteViewModel.cs | 4 +- ...uginPrerequisitesInstallDialogViewModel.cs | 8 +- ...inPrerequisitesUninstallDialogViewModel.cs | 8 +- .../ViewModels/PluginSettingsViewModel.cs | 11 +- .../PluginSettingsWindowViewModel.cs | 4 +- .../Plugins/Views/PluginFeatureView.axaml | 6 +- .../Plugins/Views/PluginFeatureView.axaml.cs | 5 +- .../Plugins/Views/PluginSettingsView.axaml | 6 +- .../Plugins/Views/PluginSettingsView.axaml.cs | 5 +- .../Views/PluginSettingsWindowView.axaml | 2 +- .../Views/PluginSettingsWindowView.axaml.cs | 4 +- .../ViewModels/ProfileEditorViewModel.cs | 8 + .../Views/ProfileEditorView.axaml | 2 +- .../Views/ProfileEditorView.axaml.cs | 4 +- .../Screens/Root/ViewModels/RootViewModel.cs | 8 +- .../ViewModels/SidebarCategoryViewModel.cs | 6 +- .../SidebarProfileConfigurationViewModel.cs | 4 +- .../Root/ViewModels/SidebarScreenViewModel.cs | 4 +- .../Root/ViewModels/SidebarViewModel.cs | 14 +- .../Screens/Root/Views/RootView.axaml | 2 +- .../Screens/Root/Views/RootView.axaml.cs | 4 +- .../Root/Views/SidebarCategoryView.axaml | 4 +- .../Root/Views/SidebarCategoryView.axaml.cs | 7 +- .../SidebarProfileConfigurationView.axaml | 6 +- .../SidebarProfileConfigurationView.axaml.cs | 2 +- .../Root/Views/SidebarScreenView.axaml | 2 +- .../Root/Views/SidebarScreenView.axaml.cs | 2 +- .../Screens/Root/Views/SidebarView.axaml | 3 +- .../Screens/Root/Views/SidebarView.axaml.cs | 4 +- .../Screens/Settings/SettingsView.axaml | 2 +- .../Screens/Settings/SettingsView.axaml.cs | 2 +- .../Screens/Settings/SettingsViewModel.cs | 6 +- .../Tabs/ViewModels/AboutTabViewModel.cs | 4 +- .../Tabs/ViewModels/DevicesTabViewModel.cs | 10 +- .../Tabs/ViewModels/GeneralTabViewModel.cs | 7 +- .../Tabs/ViewModels/PluginsTabViewModel.cs | 14 +- .../Settings/Tabs/Views/AboutTabView.axaml | 2 +- .../Settings/Tabs/Views/AboutTabView.axaml.cs | 4 +- .../Settings/Tabs/Views/DevicesTabView.axaml | 2 +- .../Tabs/Views/DevicesTabView.axaml.cs | 4 +- .../Settings/Tabs/Views/GeneralTabView.axaml | 4 +- .../Tabs/Views/GeneralTabView.axaml.cs | 4 +- .../Settings/Tabs/Views/PluginsTabView.axaml | 2 +- .../Tabs/Views/PluginsTabView.axaml.cs | 5 +- .../ViewModels/ListDeviceViewModel.cs | 4 +- .../ViewModels/SurfaceDeviceViewModel.cs | 8 +- .../ViewModels/SurfaceEditorViewModel.cs | 7 +- .../SurfaceEditor/Views/ListDeviceView.axaml | 2 +- .../Views/ListDeviceView.axaml.cs | 3 +- .../Views/SurfaceDeviceView.axaml | 4 +- .../Views/SurfaceDeviceView.axaml.cs | 4 +- .../Views/SurfaceEditorView.axaml | 4 +- .../Views/SurfaceEditorView.axaml.cs | 7 +- .../Workshop/ViewModels/WorkshopViewModel.cs | 6 +- .../Screens/Workshop/Views/WorkshopView.axaml | 4 +- .../Workshop/Views/WorkshopView.axaml.cs | 4 +- .../Artemis.UI}/Services/DebugService.cs | 8 +- .../Services/Interfaces/IArtemisUIService.cs | 2 +- .../Services/Interfaces/IDebugService.cs | 2 +- .../Interfaces/IRegistrationService.cs | 2 +- .../Services/RegistrationService.cs | 6 +- .../Artemis.UI}/Styles/Border.axaml | 0 .../Artemis.UI}/Styles/Button.axaml | 0 .../Artemis.UI}/Styles/Sidebar.axaml | 0 .../Artemis.UI}/Styles/TextBlock.axaml | 0 .../Artemis.UI}/ViewLocator.cs | 4 +- .../Artemis.UI}/packages.lock.json | 2 +- 205 files changed, 7871 insertions(+), 578 deletions(-) delete mode 100644 src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj delete mode 100644 src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs delete mode 100644 src/Artemis.UI/Artemis.UI.csproj.DotSettings create mode 100644 src/Avalonia/Artemis.UI.Linux/.gitignore create mode 100644 src/Avalonia/Artemis.UI.Linux/App.axaml create mode 100644 src/Avalonia/Artemis.UI.Linux/App.axaml.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI.Linux}/Assets/avalonia-logo.ico (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI.Linux}/Program.cs (64%) create mode 100644 src/Avalonia/Artemis.UI.Linux/packages.lock.json create mode 100644 src/Avalonia/Artemis.UI.MacOS/.gitignore create mode 100644 src/Avalonia/Artemis.UI.MacOS/App.axaml create mode 100644 src/Avalonia/Artemis.UI.MacOS/App.axaml.cs create mode 100644 src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj create mode 100644 src/Avalonia/Artemis.UI.MacOS/Assets/avalonia-logo.ico create mode 100644 src/Avalonia/Artemis.UI.MacOS/Program.cs create mode 100644 src/Avalonia/Artemis.UI.MacOS/packages.lock.json rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Artemis.UI.Avalonia.Shared.csproj.DotSettings (100%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Artemis.UI.Avalonia.Shared.xml rename src/{Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj => Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj} (91%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/ArtemisIcon.axaml (84%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/ArtemisIcon.axaml.cs (98%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/DeviceVisualizer.cs (99%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/DeviceVisualizerLed.cs (99%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/EnumComboBox.axaml (89%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/EnumComboBox.axaml.cs (97%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/ProfileConfigurationIcon.axaml (80%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/ProfileConfigurationIcon.axaml.cs (98%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Controls/SelectionRectangle.cs (99%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Converters/ColorToSKColorConverter.cs (96%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Converters/SKColorToColorConverter.cs (96%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/DataModelInputDynamicEventArgs.cs (92%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/DataModelInputStaticEventArgs.cs (91%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/DialogClosedEventArgs.cs (84%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/LedClickedEventArgs.cs (93%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/ProfileConfigurationEventArgs.cs (96%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/RenderProfileElementEventArgs.cs (96%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Events/SelectionRectangleEventArgs.cs (84%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Exceptions/ArtemisUIException.cs (90%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Ninject/SharedUIModule.cs (90%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Plugins/PluginConfigurationDialog.cs (93%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Plugins/PluginConfigurationViewModel.cs (96%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Builders/ContentDialogBuilder.cs (97%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Builders/FileDialogFilterBuilder.cs (94%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Builders/NotificationBuilder.cs (98%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Builders/OpenFileDialogBuilder.cs (97%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Builders/SaveFileDialogBuilder.cs (97%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Interfaces/IArtemisSharedUIService.cs (74%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Interfaces/INotificationService.cs (54%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/Interfaces/IWindowService.cs (96%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/NotificationService.cs (72%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/WindowService/ExceptionDialogView.axaml (97%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/WindowService/ExceptionDialogView.axaml.cs (90%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/WindowService/ExceptionDialogViewModel.cs (86%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Services/WindowService/WindowService.cs (95%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/Styles/InfoBar.axaml (100%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/ViewModelBase.cs (97%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/nuget.config (100%) rename src/{Artemis.UI.Avalonia.Shared => Avalonia/Artemis.UI.Shared}/packages.lock.json (100%) create mode 100644 src/Avalonia/Artemis.UI.Windows/.gitignore create mode 100644 src/Avalonia/Artemis.UI.Windows/App.axaml rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI.Windows}/App.axaml.cs (89%) create mode 100644 src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/avalonia-logo.ico create mode 100644 src/Avalonia/Artemis.UI.Windows/Program.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/packages.lock.json rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/App.axaml (90%) create mode 100644 src/Avalonia/Artemis.UI/App.axaml.cs rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Artemis.UI.Avalonia.csproj.DotSettings (100%) create mode 100644 src/Avalonia/Artemis.UI/Artemis.UI.csproj create mode 100644 src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Assets/Images/Logo/bow-black.ico (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Assets/Images/Logo/bow-white.ico (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Assets/Images/Logo/bow-white.svg (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Assets/Images/Logo/bow.ico (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Assets/Images/Logo/bow.svg (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Assets/Images/home-banner.png (100%) create mode 100644 src/Avalonia/Artemis.UI/Assets/avalonia-logo.ico rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Converters/ColorToSolidColorBrushConverter.cs (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Converters/EnumToCollectionConverter.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Converters/LedIdToStringConverter.cs (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Converters/NormalizedPercentageConverter.cs (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Converters/UriToFileNameConverter.cs (93%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Converters/ValuesAdditionConverter.cs (91%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Exceptions/ArtemisGraphicsContextException.cs (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Exceptions/ArtemisUIException.cs (89%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Extensions/BindableCollectionExtensions.cs (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/MainWindow.axaml (95%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/MainWindow.axaml.cs (82%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Ninject/Factories/IVMFactory.cs (82%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Ninject/UIModule.cs (88%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Providers/AvaloniaInputProvider.cs (72%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/DebugView.axaml (90%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/DebugView.axaml.cs (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/DebugViewModel.cs (86%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml (80%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs (77%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml (82%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs (77%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml (80%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs (84%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs (77%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Render/RenderDebugView.axaml (88%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs (86%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml (98%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml (81%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs (83%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/InputMappingsTabView.axaml (80%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs (91%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/ViewModels/DevicePropertiesViewModel.cs (89%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/ViewModels/DeviceSettingsViewModel.cs (93%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Views/DeviceDetectInputView.axaml (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Views/DeviceDetectInputView.axaml.cs (77%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Views/DevicePropertiesView.axaml (95%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Views/DevicePropertiesView.axaml.cs (83%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Views/DeviceSettingsView.axaml (93%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Device/Views/DeviceSettingsView.axaml.cs (82%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Home/ViewModels/HomeViewModel.cs (80%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Home/Views/HomeView.axaml (99%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Home/Views/HomeView.axaml.cs (76%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/MainScreenViewModel.cs (85%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs (76%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs (95%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs (98%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs (86%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/Views/PluginFeatureView.axaml (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/Views/PluginFeatureView.axaml.cs (73%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/Views/PluginSettingsView.axaml (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/Views/PluginSettingsView.axaml.cs (73%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/Views/PluginSettingsWindowView.axaml (89%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs (90%) create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/ProfileEditor/Views/ProfileEditorView.axaml (81%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs (74%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/ViewModels/RootViewModel.cs (79%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/ViewModels/SidebarCategoryViewModel.cs (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs (91%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/ViewModels/SidebarScreenViewModel.cs (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/ViewModels/SidebarViewModel.cs (91%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/RootView.axaml (93%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/RootView.axaml.cs (76%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarCategoryView.axaml (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarCategoryView.axaml.cs (76%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarProfileConfigurationView.axaml (94%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarProfileConfigurationView.axaml.cs (87%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarScreenView.axaml (89%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarScreenView.axaml.cs (86%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarView.axaml (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Root/Views/SidebarView.axaml.cs (76%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/SettingsView.axaml (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/SettingsView.axaml.cs (87%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/SettingsViewModel.cs (84%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/ViewModels/AboutTabViewModel.cs (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/ViewModels/GeneralTabViewModel.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/ViewModels/PluginsTabViewModel.cs (93%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/AboutTabView.axaml (99%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/AboutTabView.axaml.cs (74%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/DevicesTabView.axaml (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs (74%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/GeneralTabView.axaml (98%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/GeneralTabView.axaml.cs (74%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/PluginsTabView.axaml (88%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Settings/Tabs/Views/PluginsTabView.axaml.cs (70%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/ViewModels/ListDeviceViewModel.cs (87%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/ViewModels/SurfaceDeviceViewModel.cs (96%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/Views/ListDeviceView.axaml (81%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/Views/ListDeviceView.axaml.cs (81%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml (88%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml.cs (92%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml (97%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml.cs (93%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Workshop/ViewModels/WorkshopViewModel.cs (83%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Workshop/Views/WorkshopView.axaml (87%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Screens/Workshop/Views/WorkshopView.axaml.cs (75%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Services/DebugService.cs (82%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Services/Interfaces/IArtemisUIService.cs (50%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Services/Interfaces/IDebugService.cs (70%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Services/Interfaces/IRegistrationService.cs (86%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Services/RegistrationService.cs (86%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Styles/Border.axaml (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Styles/Button.axaml (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Styles/Sidebar.axaml (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/Styles/TextBlock.axaml (100%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/ViewLocator.cs (91%) rename src/{Artemis.UI.Avalonia => Avalonia/Artemis.UI}/packages.lock.json (99%) diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 96cbdbc6e..d827c2c72 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -1,348 +1,348 @@ - Artemis.UI.Avalonia.Shared + Artemis.UI.Shared - + Gets or sets the currently displayed icon as either a or an pointing to an SVG - + Gets or sets the currently displayed icon as either a or an pointing to an SVG - + Visualizes an with optional per-LED colors - + - + - + Occurs when a LED of the device has been clicked - + - Invokes the event + Invokes the event - + Gets or sets the to display - + Gets or sets the to display - + Gets or sets boolean indicating whether or not to show per-LED colors - + Gets or sets a boolean indicating whether or not to show per-LED colors - + Gets or sets a list of LEDs to highlight - + Gets or sets a list of LEDs to highlight - + - + - + - + Gets or sets the currently selected value - + Gets or sets the currently selected value - + - + - + Gets or sets the to display - + Gets or sets the to display - + Visualizes an with optional per-LED colors - + - Defines the property. + Defines the property. - + - Defines the property. + Defines the property. - + - Defines the property. + Defines the property. - + - Defines the property. + Defines the property. - + - + Gets or sets a brush used to paint the control's background. - + Gets or sets a brush used to paint the control's border - + Gets or sets the width of the control's border - + - + - + Converts into . - + - + - + Converts into . - + - + - + Provides data about selection events raised by - + Gets the data model path that was selected - + Provides data about submit events raised by - + The value that was submitted - + Provides data on LED click events raised by the device visualizer - + The device that was clicked - + The LED that was clicked - + Provides data on profile related events raised by the profile editor - + Gets the profile the event was raised for - + If applicable, the previous active profile before the event was raised - + Provides data on profile element related events raised by the profile editor - + Gets the profile element the event was raised for - + If applicable, the previous active profile element before the event was raised - + Represents errors that occur within the Artemis Shared UI library - + The main of the Artemis Shared UI toolkit that binds all services - + - + - + - + Describes a configuration dialog for a specific plugin - + - + Represents a view model for a plugin configuration window - + - Creates a new instance of the class + Creates a new instance of the class - + Gets the plugin this configuration view model is associated with - + Closes the window hosting the view model - + Occurs when the the window hosting the view model should close - + Represents a builder that can create a . - + Sets the name of the filter - + Adds the provided extension to the filter - + Add a filter to the dialog - + Represents a builder that can create a . - + Indicate that the user can select multiple files. - + Set the title of the dialog - + Set the initial directory of the dialog - + Set the initial file name of the dialog - + Add a filter to the dialog - + Shows the file dialog @@ -351,37 +351,37 @@ files, or null if the dialog was canceled. - + Represents a builder that can create a . - + Set the title of the dialog - + Set the initial directory of the dialog - + Set the initial file name of the dialog - + Set the default extension of the dialog - + Add a filter to the dialog - + Shows the save file dialog. @@ -390,32 +390,32 @@ dialog was canceled. - + Represents a service provided by the Artemis Shared UI library - + Creates a view model instance of type and shows its corresponding View as a window The type of view model to create The created view model - + Given a ViewModel, show its corresponding View as a window ViewModel to show the View for - + Shows a dialog displaying the given exception The title of the dialog The exception to display - + Given an existing ViewModel, show its corresponding View as a Dialog @@ -423,7 +423,7 @@ ViewModel to show the View for A task containing the return value of type - + Creates a view model instance of type and shows its corresponding View as a Dialog @@ -431,7 +431,7 @@ The return type A task containing the return value of type - + Shows a content dialog asking the user to confirm an action @@ -441,37 +441,37 @@ The text of the cancel button, if the cancel button will not be shown A task containing the result of the dialog, if confirmed; otherwise - + Creates an open file dialog, use the fluent API to configure it The builder that can be used to configure the dialog - + Creates a save file dialog, use the fluent API to configure it The builder that can be used to configure the dialog - + Represents the base class for Artemis view models - + Gets or sets the display name of the view model - + Represents the base class for Artemis view models that are interested in the activated event - + - + Releases the unmanaged resources used by the object and optionally releases the managed resources. @@ -480,24 +480,24 @@ to release only unmanaged resources. - + - + - + Represents the base class for Artemis view models used to drive dialogs - + Closes the dialog with the given The result of the dialog - + Closes the dialog without a result diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj deleted file mode 100644 index 9b363bcd7..000000000 --- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj +++ /dev/null @@ -1,117 +0,0 @@ - - - WinExe - net5.0 - enable - bin\ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - SidebarView.axaml - Code - - - RootView.axaml - Code - - - - - - - - - - - - - $(DefaultXamlRuntime) - MSBuild:Compile - - - $(DefaultXamlRuntime) - MSBuild:Compile - - - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs deleted file mode 100644 index 3d5260310..000000000 --- a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Artemis.UI.Avalonia.Shared; - -namespace Artemis.UI.Avalonia.Screens.ProfileEditor.ViewModels -{ - public class ProfileEditorViewModel : ActivatableViewModelBase - { - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs b/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs index cbc4f7413..23f1fd0ed 100644 --- a/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs +++ b/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs @@ -15,7 +15,9 @@ namespace Artemis.UI.Shared /// public abstract class PluginConfigurationDialog : IPluginConfigurationDialog { - /// + /// + /// The type of view model the tab contains + /// public abstract Type Type { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj.DotSettings b/src/Artemis.UI/Artemis.UI.csproj.DotSettings deleted file mode 100644 index 8bd53f68f..000000000 --- a/src/Artemis.UI/Artemis.UI.csproj.DotSettings +++ /dev/null @@ -1,5 +0,0 @@ - - True - True - True - True \ No newline at end of file diff --git a/src/Artemis.sln b/src/Artemis.sln index 10dd6db3f..0f40efd73 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -13,16 +13,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Shared", "Artemi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Console", "Artemis.ConsoleUI\Artemis.UI.Console.csproj", "{ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Avalonia", "Artemis.UI.Avalonia\Artemis.UI.Avalonia.csproj", "{035CBB38-7B9E-4375-A39C-E9A5B01F23A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI", "Avalonia\Artemis.UI\Artemis.UI.csproj", "{035CBB38-7B9E-4375-A39C-E9A5B01F23A5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Avalonia.Shared", "Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.csproj", "{05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Shared", "Avalonia\Artemis.UI.Shared\Artemis.UI.Shared.csproj", "{05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{CF125C61-FD85-47EE-AF64-38B8F90DD50C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WPF", "WPF", "{EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Avalonia", "Avalonia", "{960CAAC5-AA73-49F5-BF2F-DF2C789DF042}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.Windows", "Avalonia\Artemis.UI.Windows\Artemis.UI.Windows.csproj", "{DE45A288-9320-461F-BE2A-26DFE3817216}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.Linux", "Avalonia\Artemis.UI.Linux\Artemis.UI.Linux.csproj", "{9012C8E2-3BEC-42F5-8270-7352A5922B04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.MacOS", "Avalonia\Artemis.UI.MacOS\Artemis.UI.MacOS.csproj", "{2F5F16DC-FACF-4559-9882-37C2949814C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +87,30 @@ Global {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Release|Any CPU.ActiveCfg = Release|x64 {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Release|x64.ActiveCfg = Release|x64 {CF125C61-FD85-47EE-AF64-38B8F90DD50C}.Release|x64.Build.0 = Release|x64 + {DE45A288-9320-461F-BE2A-26DFE3817216}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Debug|x64.Build.0 = Debug|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Release|Any CPU.Build.0 = Release|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Release|x64.ActiveCfg = Release|Any CPU + {DE45A288-9320-461F-BE2A-26DFE3817216}.Release|x64.Build.0 = Release|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Debug|x64.ActiveCfg = Debug|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Debug|x64.Build.0 = Debug|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Release|Any CPU.Build.0 = Release|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Release|x64.ActiveCfg = Release|Any CPU + {9012C8E2-3BEC-42F5-8270-7352A5922B04}.Release|x64.Build.0 = Release|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Debug|x64.Build.0 = Debug|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|Any CPU.Build.0 = Release|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|x64.ActiveCfg = Release|Any CPU + {2F5F16DC-FACF-4559-9882-37C2949814C7}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,6 +121,9 @@ Global {035CBB38-7B9E-4375-A39C-E9A5B01F23A5} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} {CF125C61-FD85-47EE-AF64-38B8F90DD50C} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} + {DE45A288-9320-461F-BE2A-26DFE3817216} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} + {9012C8E2-3BEC-42F5-8270-7352A5922B04} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} + {2F5F16DC-FACF-4559-9882-37C2949814C7} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C203080A-4473-4CC2-844B-F552EA43D66A} diff --git a/src/Avalonia/Artemis.UI.Linux/.gitignore b/src/Avalonia/Artemis.UI.Linux/.gitignore new file mode 100644 index 000000000..8afdcb635 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/src/Avalonia/Artemis.UI.Linux/App.axaml b/src/Avalonia/Artemis.UI.Linux/App.axaml new file mode 100644 index 000000000..25cdb6886 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/App.axaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs new file mode 100644 index 000000000..99bf1b9ab --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs @@ -0,0 +1,50 @@ +using Artemis.Core.Ninject; +using Artemis.UI.Ninject; +using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Shared.Ninject; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using Ninject; +using ReactiveUI; +using Splat.Ninject; + +namespace Artemis.UI.Linux +{ + public class App : Application + { + private StandardKernel _kernel = null!; + + public override void Initialize() + { + InitializeNinject(); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow + { + DataContext = _kernel.Get() + }; + + base.OnFrameworkInitializationCompleted(); + } + + private void InitializeNinject() + { + _kernel = new StandardKernel(); + _kernel.Settings.InjectNonPublic = true; + + _kernel.Load(); + _kernel.Load(); + _kernel.Load(); + + _kernel.UseNinjectDependencyResolver(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj new file mode 100644 index 000000000..4e4e770d6 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -0,0 +1,22 @@ + + + WinExe + net5.0 + enable + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Assets/avalonia-logo.ico b/src/Avalonia/Artemis.UI.Linux/Assets/avalonia-logo.ico similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/avalonia-logo.ico rename to src/Avalonia/Artemis.UI.Linux/Assets/avalonia-logo.ico diff --git a/src/Artemis.UI.Avalonia/Program.cs b/src/Avalonia/Artemis.UI.Linux/Program.cs similarity index 64% rename from src/Artemis.UI.Avalonia/Program.cs rename to src/Avalonia/Artemis.UI.Linux/Program.cs index 3609fd618..35139d1e0 100644 --- a/src/Artemis.UI.Avalonia/Program.cs +++ b/src/Avalonia/Artemis.UI.Linux/Program.cs @@ -1,26 +1,24 @@ -using Avalonia; +using System; +using Avalonia; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia +namespace Artemis.UI.Linux { internal class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. + [STAThread] public static void Main(string[] args) { - BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() { - return AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace() - .UseReactiveUI(); + return AppBuilder.Configure().UsePlatformDetect().LogToTrace().UseReactiveUI(); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json new file mode 100644 index 000000000..bc92463a3 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -0,0 +1,1720 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Avalonia": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "dependencies": { + "Avalonia.Remote.Protocol": "0.10.10", + "JetBrains.Annotations": "10.3.0", + "System.ComponentModel.Annotations": "4.5.0", + "System.Memory": "4.5.3", + "System.Reactive": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.6.0", + "System.ValueTuple": "4.5.0" + } + }, + "Avalonia.Desktop": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Native": "0.10.10", + "Avalonia.Skia": "0.10.10", + "Avalonia.Win32": "0.10.10", + "Avalonia.X11": "0.10.10" + } + }, + "Avalonia.Diagnostics": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Controls.DataGrid": "0.10.10", + "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.ReactiveUI": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "dependencies": { + "Avalonia": "0.10.10", + "ReactiveUI": "13.2.10", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.0.2020091801", + "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + }, + "Avalonia.Controls.DataGrid": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.10", + "JetBrains.Annotations": "10.3.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Controls.PanAndZoom": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "zIQhp86CdV7xmFXFkaQBDNDr0WSyumEdJvqvIrywG5SEQK3HzACt0gR85KX19DHTlkJlnUVjmfkTEiPjwvgGtA==", + "dependencies": { + "Avalonia": "0.10.8" + } + }, + "Avalonia.FreeDesktop": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "dependencies": { + "Avalonia": "0.10.10", + "Tmds.DBus": "0.9.0" + } + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "dependencies": { + "Avalonia": "0.10.10" + } + }, + "Avalonia.Remote.Protocol": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + }, + "Avalonia.Skia": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "dependencies": { + "Avalonia": "0.10.10", + "HarfBuzzSharp": "2.6.1.7", + "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", + "SkiaSharp": "2.80.2", + "SkiaSharp.NativeAssets.Linux": "2.80.2" + } + }, + "Avalonia.Svg.Skia": { + "type": "Transitive", + "resolved": "0.10.8.3", + "contentHash": "w7RYf+8+gOI3uVZZJ59S0EP49LVsyr1jpnZQzVFQqKa3y/c/i2jT/EUoKOeaqPMhFIsQZyEF4iluqoo6aZ05Tw==", + "dependencies": { + "Avalonia": "0.10.8", + "Avalonia.Skia": "0.10.8", + "SkiaSharp": "2.80.2", + "Svg.Skia": "0.5.8.3" + } + }, + "Avalonia.Win32": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", + "System.Drawing.Common": "4.5.0", + "System.Numerics.Vectors": "4.5.0" + } + }, + "Avalonia.X11": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.FreeDesktop": "0.10.10", + "Avalonia.Skia": "0.10.10" + } + }, + "Avalonia.Xaml.Behaviors": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "rHDkieWZDTjG+PVGQzronzknmH24r2VDtzbNfC3O8FLZGqREsBoCRDrqW4R4bmtD6CqpDPBey5soBYnnDE1m3Q==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Xaml.Interactions": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10" + } + }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "bOJvciyk6kUjPx+mg6n+bwHQqRqgNiTDzTBkpokfkcWl9pMAlKvqqUe6YXWVCpKIDBjbzvkAbYa29S0ajqwFxw==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "xxWrpi0HsySczpU3Zl6c2ugbkTOs9qwqbvClfi/AKncoVbWpXv7W6J3kfQcfRlnKFwkTPjLyTYKVERIkb7kNCQ==", + "dependencies": { + "Avalonia": "0.10.10" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "DynamicData": { + "type": "Transitive", + "resolved": "7.1.1", + "contentHash": "Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "Fizzler": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "CPxuWF8EPvM0rwAtMTR5G+7EuLoGNXsEfqyx06upN9JyVALZ73KgbGn3SLFwGosifiUAXrvNHtXlUwGGytdECg==" + }, + "FluentAvaloniaUI": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "1W1VZQaCeH4/kzNM2c9yPHAVVs9lW9/09bzz1lqu7Tvu79u9JCOjwkZmR8rGC0KbyOA7twwVr2/VvB84zDZYvA==", + "dependencies": { + "Avalonia": "0.10.9", + "Avalonia.Desktop": "0.10.9", + "Avalonia.Diagnostics": "0.10.9" + } + }, + "Flurl": { + "type": "Transitive", + "resolved": "3.0.2", + "contentHash": "1/6mqdzGCTdAekbWkVZBTylCV+8g3JUSTXRBngRVR274S+RsAYNRF79GbDoDsPfMKu8VPc9HkQWdBEAncK1PQQ==" + }, + "Flurl.Http": { + "type": "Transitive", + "resolved": "3.2.0", + "contentHash": "5S8YiJm5CyRFO418GG9PDrsgmGEaZJtZLUqk3xqBKrfx7W9eKtOSpeji/GwAL+tSgNTALKBPvBKTaT4S83Oo+w==", + "dependencies": { + "Flurl": "3.0.2", + "Newtonsoft.Json": "12.0.2", + "System.Text.Encoding.CodePages": "4.5.1" + } + }, + "HarfBuzzSharp": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7" + } + }, + "HidSharp": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.11.10", + "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "0GLU9lwGVXjUNlr9ZIdAgjqLI2Zm/XFGJFaqJ1T1sU+kwfeMLhm68+rblUrNUP9psRl4i8yM7Ghb4ia4oI2E5g==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "LiteDB": { + "type": "Transitive", + "resolved": "5.0.10", + "contentHash": "x70WuqMDuP75dajqSLvO+AnI/BbwS6da+ukTO7rueV7VoXoQ5CRA9FV4r7cOS4OUr2NS1Up7LDIutjCxQycRvg==" + }, + "Live.Avalonia": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "FIzh7k2PWsgIBjS4no51ZzWxYmzTG/RzC0DUO6PzoiKkqyKpvSpOvcRg+41Roz6X8VnYCIVm61R7TFlT4eUFKA==", + "dependencies": { + "Avalonia": "0.10.0" + } + }, + "Material.Icons": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "4UIT91QbedjNfUYU+R3T60U+InozFtIsP1iUzGbkq/G0f1eDE3tXMWUuLEDO3yCEP2MHrPjAOpokwqk1rnWNGA==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + }, + "Material.Icons.Avalonia": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "hK0MQm2XyPwjT+DiviDOBjrJVQe6V0u+XTDVbxohkq58hUBlq0XZXmHHZ27jUJU6ZVP9ybu44aXfWycbVjnY2A==", + "dependencies": { + "Avalonia": "0.10.0", + "Material.Icons": "1.0.2" + } + }, + "McMaster.NETCore.Plugins": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "2.9.6", + "contentHash": "Kmms3TxGQMNb95Cu/3K+0bIcMnV4qf/phZBLAB0HUi65rBPxP4JO3aM2LoAcb+DFS600RQJMZ7ZLyYDTbLwJOQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "3ncA7cV+iXGA1VYwe2UEZXcvWyZSlbexWjM9AvocP7sik5UD93qt9Hq0fMRGk0jFRmvmE4T2g+bGfXiBVZEhLw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "2.9.6", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Metadata": "1.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "/LsTtgcMN6Tu1oo7/WYbRAHL4/ubXC/miEakwTpcZKJKtFo7D0AK95Hw0dbGxul6C8WJu60v6NP2435TDYZM+Q==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Scripting": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "tLgqc76qXHmONUhWhxo7z3TcL/LmGFWIUJm1exbQmVJohuQvJnejUMxmVkdxDfMuMZU1fIyJXPZ6Fkp4FEneAg==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.CSharp": "[3.4.0]", + "Microsoft.CodeAnalysis.Common": "[3.4.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "+b6I3DZL2zvck+B/E/aiOveakj5U2G2BcYODQxcGh2IDbatNU3XXxGT1HumkWB5uIZI2Leu0opBgBpjScmjGMA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Ninject": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", + "dependencies": { + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0" + } + }, + "Ninject.Extensions.ChildKernel": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", + "dependencies": { + "Ninject": "3.3.4" + } + }, + "Ninject.Extensions.Conventions": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", + "dependencies": { + "Ninject.Extensions.Factory": "3.3.2" + } + }, + "Ninject.Extensions.Factory": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", + "dependencies": { + "Castle.Core": "4.2.0", + "Ninject": "3.3.3" + } + }, + "ReactiveUI": { + "type": "Transitive", + "resolved": "13.2.10", + "contentHash": "fOCbEZ+RsO2Jhv6vB8VX+ZEvczYJaC95atcSG7oXohJeL/sEwbbqvv9k+tbj2l4bRSj2j5CQvhwA3HNLaxlCAg==", + "dependencies": { + "DynamicData": "7.1.1", + "Splat": "10.0.1", + "System.Reactive": "5.0.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "ShimSkiaSharp": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "BWwwsIlYUFF0DUc8Pa9xONIXVDvEL9pOYc9YmWilpHrWC37dcK+H4+tfuxztZxtfJx559HGn+6iZmMDjfFoOxA==" + }, + "SkiaSharp": { + "type": "Transitive", + "resolved": "2.80.3", + "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SkiaSharp.HarfBuzz": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7", + "SkiaSharp": "2.80.2" + } + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "dependencies": { + "SkiaSharp": "2.80.2" + } + }, + "Splat": { + "type": "Transitive", + "resolved": "13.1.30", + "contentHash": "yaj3r8CvHQwtvhfTi+dp5LpIb3c4svqe/tL6LdAS8wWP+dXAp3fTCLjYx21TrW1QBFTBJcg9lrJqDPbheSzHbA==" + }, + "Splat.Ninject": { + "type": "Transitive", + "resolved": "13.1.30", + "contentHash": "hYgyD12Syt2l8U/KccMzNUj4nmrdULjoRTF4g5Q9XtVWPrcdTYmLEdcX/prZEWaFT7vGNP6x9uFXvOlM7Jc+gg==", + "dependencies": { + "Ninject": "3.3.4", + "Splat": "13.1.30" + } + }, + "Svg.Custom": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "6FnbI4T3uCNN7DYJpfPFa4caTTJzp4YbhU3J4c/syX7wQNSeQ/1u7JZZ+dGgrRUauiWP8VsiCLKP8qinc5xI5w==", + "dependencies": { + "Fizzler": "1.2.0", + "System.Drawing.Common": "5.0.0", + "System.Memory": "4.5.3", + "System.ObjectModel": "4.3.0", + "System.ValueTuple": "4.5.0" + } + }, + "Svg.Model": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "F/rimPwV5KF64P8oofXGMwOZ0T7b3z1A9OiC4mv5OdSpLpMpUxpSwGLAOkJ5DFqQgXqVjKKLhPdjIjQBwy0AjA==", + "dependencies": { + "ShimSkiaSharp": "0.5.8.3", + "Svg.Custom": "0.5.8.3" + } + }, + "Svg.Skia": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "ajQ0aINQtEzWkqEXyJjnwqOFNusWNMHJVGrKa1ISbP21nrWJh+tApydLFVFGGjs91d7K3YOUbWDKlEzzdDQaOg==", + "dependencies": { + "SkiaSharp": "2.80.2", + "SkiaSharp.HarfBuzz": "2.80.2", + "Svg.Custom": "0.5.8.3", + "Svg.Model": "0.5.8.3" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "HxozeSlipUK7dAroTYwIcGwKDeOVpQnJlpVaOkBz7CM4TsE5b/tKlQBZecTjh6FzcSbxndYaxxpsBMz+wMJeyw==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.1.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "+MvhNtcvIbqmhANyKu91jQnvIRVSTiaOiFNfKWwXGHG48YAb4I/TyH8spsySiPYla7gKal5ZnF3teJqZAximyQ==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "Tmds.DBus": { + "type": "Transitive", + "resolved": "0.9.0", + "contentHash": "KcTWL9aKuob9Qo2sOTTKFePs1rKGTwZrcBvMFuGVIVR5RojX3oIFj5UBLYfSGjYgrcImC7LjQI3DdCFwUnhNXw==", + "dependencies": { + "System.Reflection.Emit": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, + "artemis.core": { + "type": "Project", + "dependencies": { + "Artemis.Storage": "1.0.0", + "EmbedIO": "3.4.3", + "HidSharp": "2.1.0", + "Humanizer.Core": "2.11.10", + "LiteDB": "5.0.10", + "McMaster.NETCore.Plugins": "1.4.0", + "Newtonsoft.Json": "13.0.1", + "Ninject": "3.3.4", + "Ninject.Extensions.ChildKernel": "3.3.0", + "Ninject.Extensions.Conventions": "3.3.0", + "Serilog": "2.10.0", + "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.0.0", + "SkiaSharp": "2.80.3", + "System.Buffers": "4.5.1", + "System.IO.FileSystem.AccessControl": "5.0.0", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "5.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "artemis.storage": { + "type": "Project", + "dependencies": { + "LiteDB": "5.0.10", + "Serilog": "2.10.0" + } + }, + "artemis.ui": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Artemis.UI.Shared": "1.0.0", + "Avalonia": "0.10.10", + "Avalonia.Controls.PanAndZoom": "4.2.0", + "Avalonia.Desktop": "0.10.10", + "Avalonia.Diagnostics": "0.10.10", + "Avalonia.ReactiveUI": "0.10.10", + "Avalonia.Svg.Skia": "0.10.8.3", + "FluentAvaloniaUI": "1.1.5", + "Flurl.Http": "3.2.0", + "Live.Avalonia": "1.3.1", + "Material.Icons.Avalonia": "1.0.2", + "Splat.Ninject": "13.1.30" + } + }, + "artemis.ui.shared": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Avalonia": "0.10.10", + "Avalonia.ReactiveUI": "0.10.10", + "Avalonia.Svg.Skia": "0.10.8.3", + "Avalonia.Xaml.Behaviors": "0.10.10", + "Avalonia.Xaml.Interactions": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10", + "FluentAvaloniaUI": "1.1.5", + "Material.Icons.Avalonia": "1.0.2" + } + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.MacOS/.gitignore b/src/Avalonia/Artemis.UI.MacOS/.gitignore new file mode 100644 index 000000000..8afdcb635 --- /dev/null +++ b/src/Avalonia/Artemis.UI.MacOS/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/src/Avalonia/Artemis.UI.MacOS/App.axaml b/src/Avalonia/Artemis.UI.MacOS/App.axaml new file mode 100644 index 000000000..100a21804 --- /dev/null +++ b/src/Avalonia/Artemis.UI.MacOS/App.axaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs b/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs new file mode 100644 index 000000000..49e01c8e7 --- /dev/null +++ b/src/Avalonia/Artemis.UI.MacOS/App.axaml.cs @@ -0,0 +1,50 @@ +using Artemis.Core.Ninject; +using Artemis.UI.Ninject; +using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Shared.Ninject; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using Ninject; +using ReactiveUI; +using Splat.Ninject; + +namespace Artemis.UI.MacOS +{ + public class App : Application + { + private StandardKernel _kernel = null!; + + public override void Initialize() + { + InitializeNinject(); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = new MainWindow + { + DataContext = _kernel.Get() + }; + + base.OnFrameworkInitializationCompleted(); + } + + private void InitializeNinject() + { + _kernel = new StandardKernel(); + _kernel.Settings.InjectNonPublic = true; + + _kernel.Load(); + _kernel.Load(); + _kernel.Load(); + + _kernel.UseNinjectDependencyResolver(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj new file mode 100644 index 000000000..4e4e770d6 --- /dev/null +++ b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -0,0 +1,22 @@ + + + WinExe + net5.0 + enable + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.MacOS/Assets/avalonia-logo.ico b/src/Avalonia/Artemis.UI.MacOS/Assets/avalonia-logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8d49ff9b94e52778f5324a1b87dd443a698b57 GIT binary patch literal 176111 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 zds⪻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

r@R8DskCg$QY%d=x6zD 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

!LxQh!NmF zk`(0Tj<ZW5E8&DpXim*G3_g%p~m+WPP3v>mv`TF{tA#CZlAV1iI z$sbTihG%;$dY@ID{z$-{<*^l96ez;5-yPlm9<#0Vp(DmRMjt5}mO&)*|Ao}9w1NT!U!3v{BR>pG3m4dvAv-jR< zR##iwb^`%3Z8MMRLF*iNu zmF$BJptdopP98FT%?meHALC3)l*?Eak7wgYuQrXJ_Eu;ZwiG;`^w{1|Op42fT9g|_ zsshz40(mPpNJVTIg;l?C(@J!tO!HQn8OY;fg^<T3@U|Sc83Nq zOA#FoV9fdJ-PZT9?Dwwq6Kp+yW$6+j$E+2gEKV)%rCetTnm49eyPU7DZk#q%+pW~y z|B0KsLwG6D4WtEQsJaB!E_;~4mYqb}Yk`PDRW*g$2-eD;kqA^yXCsWBh2TB7O9xEH zMx{eH8;>r~9Zc${$noP^Mu!$ThYQfe*2=(d(b)gw;QQqxDX3?It`b{AekTLUF7Gb?yx`*nAz;?yh5 z7*w$JXKH%7VCZ3i~H`= zlE&Bef$)~x%WH6)Z!*-LrE7XzE52Q^$yk%qEh{E$wz}DjokiB|S1m0hvNysa2*7yx zRs-~^|HTL(s$@{Nh>l8Kiq&75AW8<|9L$KoUWACkuW*yoq&mH?Kuqg+SJ6RO&dhv<{j z_$#!^w$(5>*r|^qC%a+CrN&>={f#9E(8 zJq;|$^KYMHGhb#$GV`L9DA&Tu9WVS=`^R-~QI^QPnguaC>2Sx$j= z-DutV&bp-LooA|saXYm0#D?5tVq=*c1p_JM)&sN!Dh&h8$0s8+pI&UM5-VKtLR^|f zR6s3K=#)Hww>x}V{+W1FY5O9Ni&J-Te0)w}mk>YIY2Ed=li}W)<=Itt!TYdl7=Sh z;#qSU=Y^){>z%OJwY9FtNm9bWx!lVEY)KUXd6US~c+`>?LTC%RKVj5XescPDCNiH? zhV{psT50&XMGX!8$9>GU06DboPbf-;-0P_;zwX6JY=spxmQ`z`k{a_CoKYvuhcgq% zgGHcIBXvS5q}ECb@4U`Uv3cE^d^1{?t!u$Wa;s?YK3?q!GiIHHU~SSePBrQbcrT5l z8M7Uyd3zr$y^wcP^4z>MVgQId{@6WNWId7%zhfB)DWt2*V6E}%sYf?CA#7BqKby!E zU`eWB^{=NNul)2nr$iTYbH8GLlSZZt!UO%d;Gt)uXYZ?PO5s?9?A(C0%~LOn+LFYj zHJ8#<=lC|m+5=%H0=Ljrao{)uL_UwRPr~}iSWP>_`@!$uN^%{oWBg@5H}L$#;Jgdf z!IXj`;4o57A;S-*j_r!|EKOdBhcG&#e_(%em3P_vDnCUDx8|3VoIqSZfy-a4*ZSFi z#>Vwd(~o)X^;%H3HdsLQ)%uy?KJ$Ks4%*DVf(g|Of%ZgWvHhXL5>dX|b=*{QpTuS< zY+o-T7n6<7X)^uUH98tYoijiGlpxU8=iLh%+h`TXTx8j@M=NbKq{zu}{EMHE&bJ&f zAvqz1V6+C09m3dYl2P1^)rViuhQ9oUch_HpDv(~XwWJ(y(Lw-#dmXkHB}mJ|u;={x zv)>iJy8ehjP`Ptfx=}ZLJrfVz4?8rtG}uDPDR%?+|^7l}C~k1Va1A4ABL*j4-5YPgh@0zLBW4rw7M~5mML5 zU!bcsd?0e)a*n=uTDm(cjd0oa)goDH2@JvjqT{!l9z5|MwH$j-HfjtOJPEe6xw&HA zDzIn5mD83mi{T8({+5!(lVr1+%ufiGfG;>$l57u3ymgDvWU zPfwnwk6F#JaGP5ya|5z-+dBNVSr)5JzkXO7?QI@E&vE$e3${65=*1sZ1fr=ziku@< zjp59%EX{DohXgIT^5uS*`h-g$`#Ch7w?%%4F6?c$ta`y)eHe%+QEVtHdycoF>h z>`C=EU!DYpHK9;x@0ss#bP)NseFhK8=&e;J!@F*eTlpZWJDr&>T+)`zhv~xpkd@?P zQ5EB<#nIV{q$a?8i@cpKmgTSwTpjDk!xJBx{Xo}3#vHxLOR>eD{GF$r|E^dK2=taUu+Z1BeUQ&{hSd$r1wazZe!Q@+T17Tv1}>tf2fd$2Nyn?U$JsM zyB!4eW0HQ#l)ln>Wt^!eESx#I=t1!u;(-OR zDi4rxJ1lGCK7(2>UCrQt9fSKQKgXhyV{00YtH5c*kiA?>!o#Y21M6-c>1(GS&vb?F z9yE4|k_CZ*+RTjnz24dQR$62HNpv3@#JNtx=XZhbXU{9Al6fbRd8hr&ge*AL^!A^9 z^U@3Zd)NFe-@uL9Ourm{4U}GIi@g{AN(4zuk}-o1#ck|)%75<}_}9?Q^QNBbQCu&D~>g|fXS&?W#NlX+5tOBn*Rw|xJT!O2)0z3GsE5^92gs8eQHcPOZ6*%RHF^Y6E_UexI^;&$1e65lU8 zzURAt@cfJ0&H~>Rci8bHc)f|H>;S7Splj*yt|YG z`VqJOyMSMgSBmLAC8MjFaf*umWyrGe)rW~+atV$GQ%a2SPii-~BUrA!*VhR?7FL`F zkiI!iUflzM>FUO%`#ASQPuj8XbDU}d<=UNWq%{tXb7F8~VgGVO4K5mhK}0gNTIuVv zbu}2c{L<(GpYN3|^iIlX*9mQd9UZw#X0FREN$XWSt}9&F9ur`kTp}?G0|&AFIObV;}q{3H7ifApmbTo~QAxrwPNsggCKC z%C*}lG-p1?$%34*^c7n7g_zzyPqGA^oWo}amf!`cU~q{Z?|mWzKzMR{I`2Bcef2*k zxF4R>En2%3i^?wLg2~Z{PVHi@}_fmO8#q58n^EZvC zaZGjXuko-eXOFYtQr9!JH92}?8jo^=?2Lwdc^`6cuoE^aT;)rJ3`V|%A-c8Bhn~(C zo-D>^p@?^gq64cD*4jHdGQavS4W?FEo_S-Lui3k^l&Wr_QUGNEe9PeO`;m`1If9`p zR~sKiEWA_qGtY{<*dVIh1)KfnS_Tq1u(iy}Dq<>!jAFkbLdhr-atXfsP5Eclsj!Rh z8|yNiG00v9i|rp@Ixc#0UZ%M=43Mp}#k1XnluVb#$j?kB7?)(1Zr#scqFzJ>;_vug zNTm^g{^aY#2)Lv+x%3)OiJ1ihh^?V(`|)~88X0WDitL_fdOaO3PK?<>K`2g)OXRF} z?yB##-p;0_70X&NTb0T+e4sUy=2sZ|@I0(e9RGZY5mGF;NzOK(^v-Cyxa%F950!K< zXS=z07oU8?QasUbu)6ZpEVK_=11R%L9!%(2PclE#2b6Z9uDD%ams-~BqD5>D6~fGH#FaxfGEwGY)El-(V?X`)dIfhir*9wFvi!M>Hd=|o zZR41~CGOd!&1y}aT=DJx+s2iu3-);X78#0PcPdkg9pokYOz{Kq*UV_+#8yYrmsTI zh{m(w`ao`s0^ek=1|crEcTPYn7kahJ4(OQ-v%^NmMG)0iou zi+A?|zsN`Y;6F{vO}W{3;57*Ww-%S8i{ZzOaK4a$lHxfWEkwRoyMLM1`ek`2ZjC{A zvN-f8)c`{;SK>zW$nR@pEwgz`s3OSY)9bdpr&<4tqZDbnA-6aL^sEtQplCDIk3;0s=4C zSZ!%Be>8dw->q-2iUW0s=WSIC)nGV-NZIE$iXud120g7GJyK8n?&JPHXRbY~8s-vz z3+}%p6wnG`w%I?P>=`{uExEOr$XyU!Fy0Hq(=quGX3a*J|FzFbTVy(MH{MLILsLY2 zAa{&!F?Y07%Iouf{w@n5wqfT89~yAFck4ae3Q;o#ypX~}wL-adz+({02ZUyt`{vt6~~LQ6sfJb|+Ap4Jx3b)0?@``W9(A4h%AknA+m4TvJ@d6JMrj=!H{KyB4lYol(CCs$yl-*OAoS>wMcd; z@=ovjFTB@vu5*4p=f3aj+@H@m_vfz9Yc|Gbo9tYDNzd*ERxWShzDYjGrzN&;m^1Ip z9@P)QcQbdi@!d)O^3P_B1?VT6ZmMSh)E~#OL$8^xduQYc?~OIQ`Fkb(%p< z)FXMnc=dddZD%ix=jBXF=iVB+fH4}ZoHm@VZLm=)URkHeRazxApF&k@=Yp&2`capl zJq*!Gu@@NB#zQjhxBm>JWl`$t5TUp}w8#)N#?qq0`K0g*Z?V>Xq9PQJdUthN}xe#%^jYy6N5K@^*a zf13LBH#zJb-5IqubNR9#`^CG$d`WKh>>P8SZ+y zGF_OG;hkX@U-b=rFEiiC7-w@mj`{msUwTWL%xUEG_nn9U?jJ|>*8!=#Yiw{ZFh|U7 zZ(f{@^d@TK#&42?G>5tV8ZHUJ@P<+!m~YBHD_IQ{gcAO(@8tu#Az@J2>|TOZUpE)# z0)H)jOzM?G3`h~;k^ZkDFZk8i!Nv;N+NmnN*}3!`Ce{(XlJRLsLW%Xo9v>Y+7OAb^ zgxebXY!45DF8#7Kem4v$b90c;_cm^TE4-1ue#XJm#n>SO%RR|C`I^n>z+WwizJ&Q6 zYDVHxX8IZ%c7M_=rj{cd>Cw2mxp6CMwBrQFxEjXHWDhp1G_sc$D<$6NS7va(VZO;K zQ$AF+N>jb;dd(AP1$bRb`Jy`sx7w+(Njy50R9D%Q9ty@8o@$20;h9)irTN5_N&N|) znV%Yjd>bbn{H&V2;odb187WnRhG4M`(c2X{MK+wm&r9mmmi!I3>zrbpE_C+2RsUX0 z9$J>tGT5|ygY0qGo4hmN{b=%KO5>8c@Y7*tqsSVzMfJy58lI^5_!drmsml>qkI;R; z|KHSyRvVqYS`sop&k%_+o#tb&?QOngM5RvygtoH+UfvT{ZMv-BD20Q1a&`jiD>Kvf zjZ6|Bqawbe!xI=nAt)sZL{glgtBn<0XiyXl7C8@7l4k%-BrtbAV^y4%#A)horoIq5 zH>PI;Cg`A}0VFWAt%!jHq&Gr-Y&kMPEUg>{ON)^VNN42=07izvU~wQydQh!mcz&)h z`m0iPi>wgVBs|A9K=Fti!fhgSL*rz)V?TKm@=<0(5ycKd10JxuQKWy4;f!WT(~1)9 znu`{+Gc12E;hLsYW@a{a4Keu=Ng$4ZrvOY#l119B;&BrY{JG%JOoMUTdky=S1(YM& z*PJw<8hpv{b#v}X>R@_>al@|#CTh)2*?*A=_b9j>puWS%x5SROG{GPRM@)`^kzr2X zZlztAG&S+BF{!sKXMR*NnVM1=7z-f^Ub(?uN2?@MR>dMSC**5jH6l0MB*Z1SZWN7P z5)nO5L7x*zl;L;Lv&uxUr0QE)tK;!lCI}v@&4DyWa+vkn+SHiSvc%84mU)4emO*gH z$RH4Oc#0^M7+e8iEdd6?sEOFfXeoe_27pwV?(IcA7z)$1UX~YlstnaEq3jBRMvfQrDbyL{vA;O|7sRvqy@88NY)8)r+6HVnUi*oRBMU6cBdIv*6h! zS?zcgJW?`L;Rxp3d5tzM#y(Y6w1>TZ32o@BwN#Mc?YPvrEYdhP=ErB`^^; z&Vi^jgnWF5EXz5pF`37rR~pvAc_R|@B)N@ms>pzgqMDcpd7^XHEwf{;@9^MzyuPm1 zank9;g=4Qj_KQA=#Q=Vx_0bg|YD31np_B*Hr>Tuwg?d(}e4Q6ez)7ytjD{SqzN%e+ z+&%ggAfLjj*V2{IF zf0V&0i0NLcpS)HMpcO0Lk``KbvZ6}y4U-fwd)3PR6+;*cAyXM=huqyi|8ZN+p{~8wLc>HfH|>i9p~F!B3>AL-QaHRQt0d|VSP2u6|W6p>ITK3rQ)EE zhvjj)cus9Mrc@Gjr7-xbfA<^L7X`_=?ItsS=skkk&iloos&DSaQG&DLT zOI{z1Zj+@%pWx#tM|eGAJiCI3+gEUhQeL)X?7UcU8@L z+#q=60Mf6 zT1pdtf`IhqRHv9bcEk|OC0=YaqEfO+h(IN8SC}!Jv^?)1IRtl&xL-OR;ZJVN2nlv~ zzh;s<$hIZG`MZ`*$v(dgDarre%yM@`=x__0TA&BK^hlV5>WJmQ(>DCAK)jz{^hRNHj_HWX4zN^0(Ci^qt_IOHLwE!ZgPy?GEn{)>N(5xCD0KrXJxI}6F zM)}xiL88)b@o0I7=Y+uR2R^wICq0fc5u5kk%RQ^J&%5j8ux7l{{M`=4droTKmPtaPfGI|6j`F z6T!vd*UKki^>*Q*5fkKDQ-Zp1Skl03k&wpH{$=K=!Tm>vANw5{dE zFGXJL^NRGI5L4#?s*dXD*e1gs7*ea5LTPED*hbKWDKVj&Bg}xlGJ9Hx3r-bL~Gy) z)(cjuf?Po7%>I)vF<0B>e(^+1PqEpklwSNDo4a_CF18wPc^Bt2S07$Y8%W+h().UsePlatformDetect().LogToTrace().UseReactiveUI(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json new file mode 100644 index 000000000..bc92463a3 --- /dev/null +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -0,0 +1,1720 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Avalonia": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "dependencies": { + "Avalonia.Remote.Protocol": "0.10.10", + "JetBrains.Annotations": "10.3.0", + "System.ComponentModel.Annotations": "4.5.0", + "System.Memory": "4.5.3", + "System.Reactive": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.6.0", + "System.ValueTuple": "4.5.0" + } + }, + "Avalonia.Desktop": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Native": "0.10.10", + "Avalonia.Skia": "0.10.10", + "Avalonia.Win32": "0.10.10", + "Avalonia.X11": "0.10.10" + } + }, + "Avalonia.Diagnostics": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Controls.DataGrid": "0.10.10", + "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.ReactiveUI": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "dependencies": { + "Avalonia": "0.10.10", + "ReactiveUI": "13.2.10", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.0.2020091801", + "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + }, + "Avalonia.Controls.DataGrid": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.10", + "JetBrains.Annotations": "10.3.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Controls.PanAndZoom": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "zIQhp86CdV7xmFXFkaQBDNDr0WSyumEdJvqvIrywG5SEQK3HzACt0gR85KX19DHTlkJlnUVjmfkTEiPjwvgGtA==", + "dependencies": { + "Avalonia": "0.10.8" + } + }, + "Avalonia.FreeDesktop": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "dependencies": { + "Avalonia": "0.10.10", + "Tmds.DBus": "0.9.0" + } + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "dependencies": { + "Avalonia": "0.10.10" + } + }, + "Avalonia.Remote.Protocol": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + }, + "Avalonia.Skia": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "dependencies": { + "Avalonia": "0.10.10", + "HarfBuzzSharp": "2.6.1.7", + "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", + "SkiaSharp": "2.80.2", + "SkiaSharp.NativeAssets.Linux": "2.80.2" + } + }, + "Avalonia.Svg.Skia": { + "type": "Transitive", + "resolved": "0.10.8.3", + "contentHash": "w7RYf+8+gOI3uVZZJ59S0EP49LVsyr1jpnZQzVFQqKa3y/c/i2jT/EUoKOeaqPMhFIsQZyEF4iluqoo6aZ05Tw==", + "dependencies": { + "Avalonia": "0.10.8", + "Avalonia.Skia": "0.10.8", + "SkiaSharp": "2.80.2", + "Svg.Skia": "0.5.8.3" + } + }, + "Avalonia.Win32": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", + "System.Drawing.Common": "4.5.0", + "System.Numerics.Vectors": "4.5.0" + } + }, + "Avalonia.X11": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.FreeDesktop": "0.10.10", + "Avalonia.Skia": "0.10.10" + } + }, + "Avalonia.Xaml.Behaviors": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "rHDkieWZDTjG+PVGQzronzknmH24r2VDtzbNfC3O8FLZGqREsBoCRDrqW4R4bmtD6CqpDPBey5soBYnnDE1m3Q==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Xaml.Interactions": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10" + } + }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "bOJvciyk6kUjPx+mg6n+bwHQqRqgNiTDzTBkpokfkcWl9pMAlKvqqUe6YXWVCpKIDBjbzvkAbYa29S0ajqwFxw==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "xxWrpi0HsySczpU3Zl6c2ugbkTOs9qwqbvClfi/AKncoVbWpXv7W6J3kfQcfRlnKFwkTPjLyTYKVERIkb7kNCQ==", + "dependencies": { + "Avalonia": "0.10.10" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "DynamicData": { + "type": "Transitive", + "resolved": "7.1.1", + "contentHash": "Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "Fizzler": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "CPxuWF8EPvM0rwAtMTR5G+7EuLoGNXsEfqyx06upN9JyVALZ73KgbGn3SLFwGosifiUAXrvNHtXlUwGGytdECg==" + }, + "FluentAvaloniaUI": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "1W1VZQaCeH4/kzNM2c9yPHAVVs9lW9/09bzz1lqu7Tvu79u9JCOjwkZmR8rGC0KbyOA7twwVr2/VvB84zDZYvA==", + "dependencies": { + "Avalonia": "0.10.9", + "Avalonia.Desktop": "0.10.9", + "Avalonia.Diagnostics": "0.10.9" + } + }, + "Flurl": { + "type": "Transitive", + "resolved": "3.0.2", + "contentHash": "1/6mqdzGCTdAekbWkVZBTylCV+8g3JUSTXRBngRVR274S+RsAYNRF79GbDoDsPfMKu8VPc9HkQWdBEAncK1PQQ==" + }, + "Flurl.Http": { + "type": "Transitive", + "resolved": "3.2.0", + "contentHash": "5S8YiJm5CyRFO418GG9PDrsgmGEaZJtZLUqk3xqBKrfx7W9eKtOSpeji/GwAL+tSgNTALKBPvBKTaT4S83Oo+w==", + "dependencies": { + "Flurl": "3.0.2", + "Newtonsoft.Json": "12.0.2", + "System.Text.Encoding.CodePages": "4.5.1" + } + }, + "HarfBuzzSharp": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7" + } + }, + "HidSharp": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.11.10", + "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "0GLU9lwGVXjUNlr9ZIdAgjqLI2Zm/XFGJFaqJ1T1sU+kwfeMLhm68+rblUrNUP9psRl4i8yM7Ghb4ia4oI2E5g==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "LiteDB": { + "type": "Transitive", + "resolved": "5.0.10", + "contentHash": "x70WuqMDuP75dajqSLvO+AnI/BbwS6da+ukTO7rueV7VoXoQ5CRA9FV4r7cOS4OUr2NS1Up7LDIutjCxQycRvg==" + }, + "Live.Avalonia": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "FIzh7k2PWsgIBjS4no51ZzWxYmzTG/RzC0DUO6PzoiKkqyKpvSpOvcRg+41Roz6X8VnYCIVm61R7TFlT4eUFKA==", + "dependencies": { + "Avalonia": "0.10.0" + } + }, + "Material.Icons": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "4UIT91QbedjNfUYU+R3T60U+InozFtIsP1iUzGbkq/G0f1eDE3tXMWUuLEDO3yCEP2MHrPjAOpokwqk1rnWNGA==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + }, + "Material.Icons.Avalonia": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "hK0MQm2XyPwjT+DiviDOBjrJVQe6V0u+XTDVbxohkq58hUBlq0XZXmHHZ27jUJU6ZVP9ybu44aXfWycbVjnY2A==", + "dependencies": { + "Avalonia": "0.10.0", + "Material.Icons": "1.0.2" + } + }, + "McMaster.NETCore.Plugins": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "2.9.6", + "contentHash": "Kmms3TxGQMNb95Cu/3K+0bIcMnV4qf/phZBLAB0HUi65rBPxP4JO3aM2LoAcb+DFS600RQJMZ7ZLyYDTbLwJOQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "3ncA7cV+iXGA1VYwe2UEZXcvWyZSlbexWjM9AvocP7sik5UD93qt9Hq0fMRGk0jFRmvmE4T2g+bGfXiBVZEhLw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "2.9.6", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Metadata": "1.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "/LsTtgcMN6Tu1oo7/WYbRAHL4/ubXC/miEakwTpcZKJKtFo7D0AK95Hw0dbGxul6C8WJu60v6NP2435TDYZM+Q==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Scripting": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "tLgqc76qXHmONUhWhxo7z3TcL/LmGFWIUJm1exbQmVJohuQvJnejUMxmVkdxDfMuMZU1fIyJXPZ6Fkp4FEneAg==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.CSharp": "[3.4.0]", + "Microsoft.CodeAnalysis.Common": "[3.4.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "+b6I3DZL2zvck+B/E/aiOveakj5U2G2BcYODQxcGh2IDbatNU3XXxGT1HumkWB5uIZI2Leu0opBgBpjScmjGMA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Ninject": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", + "dependencies": { + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0" + } + }, + "Ninject.Extensions.ChildKernel": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", + "dependencies": { + "Ninject": "3.3.4" + } + }, + "Ninject.Extensions.Conventions": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", + "dependencies": { + "Ninject.Extensions.Factory": "3.3.2" + } + }, + "Ninject.Extensions.Factory": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", + "dependencies": { + "Castle.Core": "4.2.0", + "Ninject": "3.3.3" + } + }, + "ReactiveUI": { + "type": "Transitive", + "resolved": "13.2.10", + "contentHash": "fOCbEZ+RsO2Jhv6vB8VX+ZEvczYJaC95atcSG7oXohJeL/sEwbbqvv9k+tbj2l4bRSj2j5CQvhwA3HNLaxlCAg==", + "dependencies": { + "DynamicData": "7.1.1", + "Splat": "10.0.1", + "System.Reactive": "5.0.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "ShimSkiaSharp": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "BWwwsIlYUFF0DUc8Pa9xONIXVDvEL9pOYc9YmWilpHrWC37dcK+H4+tfuxztZxtfJx559HGn+6iZmMDjfFoOxA==" + }, + "SkiaSharp": { + "type": "Transitive", + "resolved": "2.80.3", + "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SkiaSharp.HarfBuzz": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7", + "SkiaSharp": "2.80.2" + } + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "dependencies": { + "SkiaSharp": "2.80.2" + } + }, + "Splat": { + "type": "Transitive", + "resolved": "13.1.30", + "contentHash": "yaj3r8CvHQwtvhfTi+dp5LpIb3c4svqe/tL6LdAS8wWP+dXAp3fTCLjYx21TrW1QBFTBJcg9lrJqDPbheSzHbA==" + }, + "Splat.Ninject": { + "type": "Transitive", + "resolved": "13.1.30", + "contentHash": "hYgyD12Syt2l8U/KccMzNUj4nmrdULjoRTF4g5Q9XtVWPrcdTYmLEdcX/prZEWaFT7vGNP6x9uFXvOlM7Jc+gg==", + "dependencies": { + "Ninject": "3.3.4", + "Splat": "13.1.30" + } + }, + "Svg.Custom": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "6FnbI4T3uCNN7DYJpfPFa4caTTJzp4YbhU3J4c/syX7wQNSeQ/1u7JZZ+dGgrRUauiWP8VsiCLKP8qinc5xI5w==", + "dependencies": { + "Fizzler": "1.2.0", + "System.Drawing.Common": "5.0.0", + "System.Memory": "4.5.3", + "System.ObjectModel": "4.3.0", + "System.ValueTuple": "4.5.0" + } + }, + "Svg.Model": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "F/rimPwV5KF64P8oofXGMwOZ0T7b3z1A9OiC4mv5OdSpLpMpUxpSwGLAOkJ5DFqQgXqVjKKLhPdjIjQBwy0AjA==", + "dependencies": { + "ShimSkiaSharp": "0.5.8.3", + "Svg.Custom": "0.5.8.3" + } + }, + "Svg.Skia": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "ajQ0aINQtEzWkqEXyJjnwqOFNusWNMHJVGrKa1ISbP21nrWJh+tApydLFVFGGjs91d7K3YOUbWDKlEzzdDQaOg==", + "dependencies": { + "SkiaSharp": "2.80.2", + "SkiaSharp.HarfBuzz": "2.80.2", + "Svg.Custom": "0.5.8.3", + "Svg.Model": "0.5.8.3" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "HxozeSlipUK7dAroTYwIcGwKDeOVpQnJlpVaOkBz7CM4TsE5b/tKlQBZecTjh6FzcSbxndYaxxpsBMz+wMJeyw==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.1.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "+MvhNtcvIbqmhANyKu91jQnvIRVSTiaOiFNfKWwXGHG48YAb4I/TyH8spsySiPYla7gKal5ZnF3teJqZAximyQ==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "Tmds.DBus": { + "type": "Transitive", + "resolved": "0.9.0", + "contentHash": "KcTWL9aKuob9Qo2sOTTKFePs1rKGTwZrcBvMFuGVIVR5RojX3oIFj5UBLYfSGjYgrcImC7LjQI3DdCFwUnhNXw==", + "dependencies": { + "System.Reflection.Emit": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, + "artemis.core": { + "type": "Project", + "dependencies": { + "Artemis.Storage": "1.0.0", + "EmbedIO": "3.4.3", + "HidSharp": "2.1.0", + "Humanizer.Core": "2.11.10", + "LiteDB": "5.0.10", + "McMaster.NETCore.Plugins": "1.4.0", + "Newtonsoft.Json": "13.0.1", + "Ninject": "3.3.4", + "Ninject.Extensions.ChildKernel": "3.3.0", + "Ninject.Extensions.Conventions": "3.3.0", + "Serilog": "2.10.0", + "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.0.0", + "SkiaSharp": "2.80.3", + "System.Buffers": "4.5.1", + "System.IO.FileSystem.AccessControl": "5.0.0", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "5.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "artemis.storage": { + "type": "Project", + "dependencies": { + "LiteDB": "5.0.10", + "Serilog": "2.10.0" + } + }, + "artemis.ui": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Artemis.UI.Shared": "1.0.0", + "Avalonia": "0.10.10", + "Avalonia.Controls.PanAndZoom": "4.2.0", + "Avalonia.Desktop": "0.10.10", + "Avalonia.Diagnostics": "0.10.10", + "Avalonia.ReactiveUI": "0.10.10", + "Avalonia.Svg.Skia": "0.10.8.3", + "FluentAvaloniaUI": "1.1.5", + "Flurl.Http": "3.2.0", + "Live.Avalonia": "1.3.1", + "Material.Icons.Avalonia": "1.0.2", + "Splat.Ninject": "13.1.30" + } + }, + "artemis.ui.shared": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Avalonia": "0.10.10", + "Avalonia.ReactiveUI": "0.10.10", + "Avalonia.Svg.Skia": "0.10.8.3", + "Avalonia.Xaml.Behaviors": "0.10.10", + "Avalonia.Xaml.Interactions": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10", + "FluentAvaloniaUI": "1.1.5", + "Material.Icons.Avalonia": "1.0.2" + } + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings similarity index 100% rename from src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings rename to src/Avalonia/Artemis.UI.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Avalonia.Shared.xml new file mode 100644 index 000000000..022dea7c7 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Avalonia.Shared.xml @@ -0,0 +1,506 @@ + + + + Artemis.UI.Shared + + + + + Gets or sets the currently displayed icon as either a or an pointing + to an SVG + + + + + Gets or sets the currently displayed icon as either a or an pointing + to an SVG + + + + + Visualizes an with optional per-LED colors + + + + + + + + + + + Occurs when a LED of the device has been clicked + + + + + Invokes the event + + + + + + Gets or sets the to display + + + + + Gets or sets the to display + + + + + Gets or sets boolean indicating whether or not to show per-LED colors + + + + + Gets or sets a boolean indicating whether or not to show per-LED colors + + + + + Gets or sets a list of LEDs to highlight + + + + + Gets or sets a list of LEDs to highlight + + + + + + + + + + + + + + Gets or sets the currently selected value + + + + + Gets or sets the currently selected value + + + + + + + + + + + Gets or sets the to display + + + + + Gets or sets the to display + + + + + Visualizes an with optional per-LED colors + + + + + Defines the property. + + + + + Defines the property. + + + + + Defines the property. + + + + + Defines the property. + + + + + + + + Gets or sets a brush used to paint the control's background. + + + + + Gets or sets a brush used to paint the control's border + + + + + Gets or sets the width of the control's border + + + + + + + + + + + Converts into . + + + + + + + + + + + Converts into . + + + + + + + + + + + Provides data about selection events raised by + + + + + Gets the data model path that was selected + + + + + Provides data about submit events raised by + + + + + The value that was submitted + + + + + Provides data on LED click events raised by the device visualizer + + + + + The device that was clicked + + + + + The LED that was clicked + + + + + Provides data on profile related events raised by the profile editor + + + + + Gets the profile the event was raised for + + + + + If applicable, the previous active profile before the event was raised + + + + + Provides data on profile element related events raised by the profile editor + + + + + Gets the profile element the event was raised for + + + + + If applicable, the previous active profile element before the event was raised + + + + + Represents errors that occur within the Artemis Shared UI library + + + + + The main of the Artemis Shared UI toolkit that binds all services + + + + + + + + + + + + + + Describes a configuration dialog for a specific plugin + + + + + + + + Represents a view model for a plugin configuration window + + + + + Creates a new instance of the class + + + + + + Gets the plugin this configuration view model is associated with + + + + + Closes the window hosting the view model + + + + + Occurs when the the window hosting the view model should close + + + + + Represents a builder that can create a . + + + + + Sets the name of the filter + + + + + Adds the provided extension to the filter + + + + + Add a filter to the dialog + + + + + Represents a builder that can create a . + + + + + Indicate that the user can select multiple files. + + + + + Set the title of the dialog + + + + + Set the initial directory of the dialog + + + + + Set the initial file name of the dialog + + + + + Add a filter to the dialog + + + + + Shows the file dialog + + + A task that on completion returns an array containing the full path to the selected + files, or null if the dialog was canceled. + + + + + Represents a builder that can create a . + + + + + Set the title of the dialog + + + + + Set the initial directory of the dialog + + + + + Set the initial file name of the dialog + + + + + Set the default extension of the dialog + + + + + Add a filter to the dialog + + + + + Shows the save file dialog. + + + A task that on completion contains the full path of the save location, or null if the + dialog was canceled. + + + + + Represents a service provided by the Artemis Shared UI library + + + + + Creates a view model instance of type and shows its corresponding View as a window + + The type of view model to create + The created view model + + + + Given a ViewModel, show its corresponding View as a window + + ViewModel to show the View for + + + + Shows a dialog displaying the given exception + + The title of the dialog + The exception to display + + + + Given an existing ViewModel, show its corresponding View as a Dialog + + The return type + ViewModel to show the View for + A task containing the return value of type + + + + Creates a view model instance of type and shows its corresponding View as a Dialog + + The view model type + The return type + A task containing the return value of type + + + + Shows a content dialog asking the user to confirm an action + + The title of the dialog + The message of the dialog + The text of the confirm button + The text of the cancel button, if the cancel button will not be shown + A task containing the result of the dialog, if confirmed; otherwise + + + + Creates an open file dialog, use the fluent API to configure it + + The builder that can be used to configure the dialog + + + + Creates a save file dialog, use the fluent API to configure it + + The builder that can be used to configure the dialog + + + + Represents the base class for Artemis view models + + + + + Gets or sets the display name of the view model + + + + + Represents the base class for Artemis view models that are interested in the activated event + + + + + + + + Releases the unmanaged resources used by the object and optionally releases the managed resources. + + + to release both managed and unmanaged resources; + to release only unmanaged resources. + + + + + + + + + + + Represents the base class for Artemis view models used to drive dialogs + + + + + Closes the dialog with the given + + The result of the dialog + + + + Closes the dialog without a result + + + + diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj similarity index 91% rename from src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj rename to src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index a13b34d2c..681285d01 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -21,14 +21,14 @@ - + ..\..\..\..\Users\Robert\.nuget\packages\material.icons.avalonia\1.0.2\lib\netstandard2.0\Material.Icons.Avalonia.dll - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + ..\..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings new file mode 100644 index 000000000..5230616f2 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml similarity index 84% rename from src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml rename to src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml index 18cabf7e7..4245d6b76 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Shared.Controls.ArtemisIcon"> + x:Class="Artemis.UI.Shared.Controls.ArtemisIcon"> Welcome to Avalonia! diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs similarity index 98% rename from src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs index e0914bb4c..8ea3417ad 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/ArtemisIcon.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs @@ -1,7 +1,6 @@ using System; using Avalonia; using Avalonia.Controls; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.Media.Imaging; @@ -9,7 +8,7 @@ using Avalonia.Svg.Skia; using Material.Icons; using Material.Icons.Avalonia; -namespace Artemis.UI.Avalonia.Shared.Controls +namespace Artemis.UI.Shared.Controls { public partial class ArtemisIcon : UserControl { diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs similarity index 99% rename from src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index af925a65d..2e6e832ea 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -6,7 +6,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Avalonia.Shared.Events; +using Artemis.UI.Shared.Events; using Avalonia; using Avalonia.Controls; using Avalonia.Input; @@ -18,7 +18,7 @@ using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.Visuals.Media.Imaging; -namespace Artemis.UI.Avalonia.Shared.Controls +namespace Artemis.UI.Shared.Controls { /// /// Visualizes an with optional per-LED colors diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs similarity index 99% rename from src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 966e306e5..9ccc4bcb6 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -11,7 +11,7 @@ using Color = Avalonia.Media.Color; using Point = Avalonia.Point; using SolidColorBrush = Avalonia.Media.SolidColorBrush; -namespace Artemis.UI.Avalonia.Shared.Controls +namespace Artemis.UI.Shared.Controls { internal class DeviceVisualizerLed { diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/EnumComboBox.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml similarity index 89% rename from src/Artemis.UI.Avalonia.Shared/Controls/EnumComboBox.axaml rename to src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml index 04bcd5264..bf79ed332 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/EnumComboBox.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Shared.Controls.EnumComboBox"> + x:Class="Artemis.UI.Shared.Controls.EnumComboBox"> diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/EnumComboBox.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs similarity index 97% rename from src/Artemis.UI.Avalonia.Shared/Controls/EnumComboBox.axaml.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs index 5616cc718..48ca943c2 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/EnumComboBox.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core; @@ -9,7 +8,7 @@ using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Avalonia.Shared.Controls +namespace Artemis.UI.Shared.Controls { public partial class EnumComboBox : UserControl { diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml similarity index 80% rename from src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml rename to src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml index e16baab81..3c1e984bc 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml @@ -3,5 +3,5 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Shared.Controls.ProfileConfigurationIcon"> + x:Class="Artemis.UI.Shared.Controls.ProfileConfigurationIcon"> \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs similarity index 98% rename from src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs index 83be72c21..21cd7173e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -10,7 +10,7 @@ using Avalonia.Svg.Skia; using Material.Icons; using Material.Icons.Avalonia; -namespace Artemis.UI.Avalonia.Shared.Controls +namespace Artemis.UI.Shared.Controls { public class ProfileConfigurationIcon : UserControl { diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs similarity index 99% rename from src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs rename to src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index cfd0c74b0..66257929c 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -7,7 +7,7 @@ using Avalonia.Input; using Avalonia.Media; using FluentAvalonia.Styling; -namespace Artemis.UI.Avalonia.Shared.Controls +namespace Artemis.UI.Shared.Controls { /// /// Visualizes an with optional per-LED colors diff --git a/src/Artemis.UI.Avalonia.Shared/Converters/ColorToSKColorConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs similarity index 96% rename from src/Artemis.UI.Avalonia.Shared/Converters/ColorToSKColorConverter.cs rename to src/Avalonia/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs index 728b6cc55..bbd323daa 100644 --- a/src/Artemis.UI.Avalonia.Shared/Converters/ColorToSKColorConverter.cs +++ b/src/Avalonia/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs @@ -5,7 +5,7 @@ using Avalonia.Media; using FluentAvalonia.UI.Media; using SkiaSharp; -namespace Artemis.UI.Avalonia.Shared.Converters +namespace Artemis.UI.Shared.Converters { /// /// Converts into . diff --git a/src/Artemis.UI.Avalonia.Shared/Converters/SKColorToColorConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs similarity index 96% rename from src/Artemis.UI.Avalonia.Shared/Converters/SKColorToColorConverter.cs rename to src/Avalonia/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs index 78968e4f9..ae1143a9e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Converters/SKColorToColorConverter.cs +++ b/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs @@ -5,7 +5,7 @@ using Avalonia.Media; using FluentAvalonia.UI.Media; using SkiaSharp; -namespace Artemis.UI.Avalonia.Shared.Converters +namespace Artemis.UI.Shared.Converters { /// /// Converts into . diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs similarity index 92% rename from src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs index 596f7a3c2..537b7ec11 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputDynamicEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { /// /// Provides data about selection events raised by diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs similarity index 91% rename from src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs index 2224d5021..beba22fae 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/DataModelInputStaticEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { /// /// Provides data about submit events raised by diff --git a/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs similarity index 84% rename from src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs index 0a5aa0755..c8d74c03e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/DialogClosedEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { internal class DialogClosedEventArgs : EventArgs { diff --git a/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/LedClickedEventArgs.cs similarity index 93% rename from src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/LedClickedEventArgs.cs index e6f763a25..cfaa9a160 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/LedClickedEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/LedClickedEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { /// /// Provides data on LED click events raised by the device visualizer diff --git a/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs similarity index 96% rename from src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs index ce9e24a5d..fc79d770b 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/ProfileConfigurationEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { /// /// Provides data on profile related events raised by the profile editor diff --git a/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs similarity index 96% rename from src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs index 95455d818..67cd4d5ba 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/RenderProfileElementEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { /// /// Provides data on profile element related events raised by the profile editor diff --git a/src/Artemis.UI.Avalonia.Shared/Events/SelectionRectangleEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs similarity index 84% rename from src/Artemis.UI.Avalonia.Shared/Events/SelectionRectangleEventArgs.cs rename to src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs index 3663e3f7d..2402b0858 100644 --- a/src/Artemis.UI.Avalonia.Shared/Events/SelectionRectangleEventArgs.cs +++ b/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs @@ -1,7 +1,7 @@ using System; using Avalonia; -namespace Artemis.UI.Avalonia.Shared.Events +namespace Artemis.UI.Shared.Events { public class SelectionRectangleEventArgs : EventArgs { diff --git a/src/Artemis.UI.Avalonia.Shared/Exceptions/ArtemisUIException.cs b/src/Avalonia/Artemis.UI.Shared/Exceptions/ArtemisUIException.cs similarity index 90% rename from src/Artemis.UI.Avalonia.Shared/Exceptions/ArtemisUIException.cs rename to src/Avalonia/Artemis.UI.Shared/Exceptions/ArtemisUIException.cs index c074020b7..deac21224 100644 --- a/src/Artemis.UI.Avalonia.Shared/Exceptions/ArtemisUIException.cs +++ b/src/Avalonia/Artemis.UI.Shared/Exceptions/ArtemisUIException.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Avalonia.Shared.Exceptions +namespace Artemis.UI.Shared.Exceptions { /// /// Represents errors that occur within the Artemis Shared UI library diff --git a/src/Artemis.UI.Avalonia.Shared/Ninject/SharedUIModule.cs b/src/Avalonia/Artemis.UI.Shared/Ninject/SharedUIModule.cs similarity index 90% rename from src/Artemis.UI.Avalonia.Shared/Ninject/SharedUIModule.cs rename to src/Avalonia/Artemis.UI.Shared/Ninject/SharedUIModule.cs index ed8a56783..b3469aa43 100644 --- a/src/Artemis.UI.Avalonia.Shared/Ninject/SharedUIModule.cs +++ b/src/Avalonia/Artemis.UI.Shared/Ninject/SharedUIModule.cs @@ -1,9 +1,9 @@ using System; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.Interfaces; using Ninject.Extensions.Conventions; using Ninject.Modules; -namespace Artemis.UI.Avalonia.Shared.Ninject +namespace Artemis.UI.Shared.Ninject { /// /// The main of the Artemis Shared UI toolkit that binds all services diff --git a/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationDialog.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs similarity index 93% rename from src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationDialog.cs rename to src/Avalonia/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs index 48736d54e..cbc4f7413 100644 --- a/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationDialog.cs +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Avalonia.Shared +namespace Artemis.UI.Shared { /// public class PluginConfigurationDialog : PluginConfigurationDialog where T : PluginConfigurationViewModel diff --git a/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs similarity index 96% rename from src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs rename to src/Avalonia/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs index 194ab7eec..813e195a6 100644 --- a/src/Artemis.UI.Avalonia.Shared/Plugins/PluginConfigurationViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Avalonia.Shared +namespace Artemis.UI.Shared { /// /// Represents a view model for a plugin configuration window diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs similarity index 97% rename from src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs index 9cf160b09..c2a5d967f 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/ContentDialogBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; @@ -8,7 +7,7 @@ using FluentAvalonia.UI.Controls; using Ninject; using Ninject.Parameters; -namespace Artemis.UI.Avalonia.Shared.Services.Builders +namespace Artemis.UI.Shared.Services.Builders { public class ContentDialogBuilder { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/FileDialogFilterBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs similarity index 94% rename from src/Artemis.UI.Avalonia.Shared/Services/Builders/FileDialogFilterBuilder.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs index 03cd4c7bf..a44a0b976 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/FileDialogFilterBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs @@ -1,6 +1,6 @@ using Avalonia.Controls; -namespace Artemis.UI.Avalonia.Shared.Services.Builders +namespace Artemis.UI.Shared.Services.Builders { /// /// Represents a builder that can create a . diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs similarity index 98% rename from src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs index ebfbddbff..266f71062 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs @@ -7,7 +7,7 @@ using FluentAvalonia.UI.Controls; using ReactiveUI; using Button = Avalonia.Controls.Button; -namespace Artemis.UI.Avalonia.Shared.Services.Builders +namespace Artemis.UI.Shared.Services.Builders { public class NotificationBuilder { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/OpenFileDialogBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs similarity index 97% rename from src/Artemis.UI.Avalonia.Shared/Services/Builders/OpenFileDialogBuilder.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs index f53729300..af6d98ae3 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/OpenFileDialogBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Avalonia.Controls; -namespace Artemis.UI.Avalonia.Shared.Services.Builders +namespace Artemis.UI.Shared.Services.Builders { /// /// Represents a builder that can create a . diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/SaveFileDialogBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs similarity index 97% rename from src/Artemis.UI.Avalonia.Shared/Services/Builders/SaveFileDialogBuilder.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs index c8547b9cf..e9bc452c1 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/SaveFileDialogBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Avalonia.Controls; -namespace Artemis.UI.Avalonia.Shared.Services.Builders +namespace Artemis.UI.Shared.Services.Builders { /// /// Represents a builder that can create a . diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IArtemisSharedUIService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IArtemisSharedUIService.cs similarity index 74% rename from src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IArtemisSharedUIService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IArtemisSharedUIService.cs index 464c6bc41..77567167f 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IArtemisSharedUIService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IArtemisSharedUIService.cs @@ -1,4 +1,4 @@ -namespace Artemis.UI.Avalonia.Shared.Services.Interfaces +namespace Artemis.UI.Shared.Services.Interfaces { /// /// Represents a service provided by the Artemis Shared UI library diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/INotificationService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/INotificationService.cs similarity index 54% rename from src/Artemis.UI.Avalonia.Shared/Services/Interfaces/INotificationService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Interfaces/INotificationService.cs index 6249079b8..580e1d99d 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/INotificationService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/INotificationService.cs @@ -1,6 +1,6 @@ -using Artemis.UI.Avalonia.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Builders; -namespace Artemis.UI.Avalonia.Shared.Services.Interfaces +namespace Artemis.UI.Shared.Services.Interfaces { public interface INotificationService : IArtemisSharedUIService { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs similarity index 96% rename from src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs index 8efcda80a..5c63797bb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; -using Artemis.UI.Avalonia.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Builders; using Avalonia.Controls; -namespace Artemis.UI.Avalonia.Shared.Services.Interfaces +namespace Artemis.UI.Shared.Services.Interfaces { public interface IWindowService : IArtemisSharedUIService { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/NotificationService.cs b/src/Avalonia/Artemis.UI.Shared/Services/NotificationService.cs similarity index 72% rename from src/Artemis.UI.Avalonia.Shared/Services/NotificationService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/NotificationService.cs index 16aeb45c4..224fbf0cb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/NotificationService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NotificationService.cs @@ -1,7 +1,7 @@ -using Artemis.UI.Avalonia.Shared.Services.Builders; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Avalonia.Shared.Services +namespace Artemis.UI.Shared.Services { public class NotificationService : INotificationService { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml similarity index 97% rename from src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml rename to src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml index 8211265bb..5948cba7b 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" - x:Class="Artemis.UI.Avalonia.Shared.Services.ExceptionDialogView" + x:Class="Artemis.UI.Shared.Services.ExceptionDialogView" Title="{Binding Title}" ExtendClientAreaToDecorationsHint="True" Width="800" diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml.cs similarity index 90% rename from src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs rename to src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml.cs index 9af31290c..26875428b 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogView.axaml.cs @@ -2,7 +2,7 @@ using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Shared.Services +namespace Artemis.UI.Shared.Services { internal class ExceptionDialogView : ReactiveWindow { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogViewModel.cs similarity index 86% rename from src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs rename to src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogViewModel.cs index 66fb9bdeb..093c9e53e 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/ExceptionDialogViewModel.cs @@ -1,11 +1,11 @@ using System; using System.Threading.Tasks; -using Artemis.UI.Avalonia.Shared.Services.Builders; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Interfaces; using Avalonia; using Avalonia.Layout; -namespace Artemis.UI.Avalonia.Shared.Services +namespace Artemis.UI.Shared.Services { internal class ExceptionDialogViewModel : DialogViewModelBase { diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs similarity index 95% rename from src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs rename to src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs index 9cb66b8a7..a8064cbfa 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs @@ -1,10 +1,9 @@ using System; using System.Linq; -using System.Reactive.Disposables; using System.Threading.Tasks; -using Artemis.UI.Avalonia.Shared.Exceptions; -using Artemis.UI.Avalonia.Shared.Services.Builders; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared.Exceptions; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Interfaces; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -12,9 +11,8 @@ using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Ninject; using Ninject.Parameters; -using ReactiveUI; -namespace Artemis.UI.Avalonia.Shared.Services +namespace Artemis.UI.Shared.Services { internal class WindowService : IWindowService { diff --git a/src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/InfoBar.axaml similarity index 100% rename from src/Artemis.UI.Avalonia.Shared/Styles/InfoBar.axaml rename to src/Avalonia/Artemis.UI.Shared/Styles/InfoBar.axaml diff --git a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs similarity index 97% rename from src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs rename to src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs index 1bb568963..5ce9ad05a 100644 --- a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs +++ b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs @@ -1,9 +1,9 @@ using System; using System.Reactive.Disposables; -using Artemis.UI.Avalonia.Shared.Events; +using Artemis.UI.Shared.Events; using ReactiveUI; -namespace Artemis.UI.Avalonia.Shared +namespace Artemis.UI.Shared { /// /// Represents the base class for Artemis view models diff --git a/src/Artemis.UI.Avalonia.Shared/nuget.config b/src/Avalonia/Artemis.UI.Shared/nuget.config similarity index 100% rename from src/Artemis.UI.Avalonia.Shared/nuget.config rename to src/Avalonia/Artemis.UI.Shared/nuget.config diff --git a/src/Artemis.UI.Avalonia.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json similarity index 100% rename from src/Artemis.UI.Avalonia.Shared/packages.lock.json rename to src/Avalonia/Artemis.UI.Shared/packages.lock.json diff --git a/src/Avalonia/Artemis.UI.Windows/.gitignore b/src/Avalonia/Artemis.UI.Windows/.gitignore new file mode 100644 index 000000000..8afdcb635 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/src/Avalonia/Artemis.UI.Windows/App.axaml b/src/Avalonia/Artemis.UI.Windows/App.axaml new file mode 100644 index 000000000..c0d54a0e2 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI.Avalonia/App.axaml.cs b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs similarity index 89% rename from src/Artemis.UI.Avalonia/App.axaml.cs rename to src/Avalonia/Artemis.UI.Windows/App.axaml.cs index ff5b99efb..4e72ab276 100644 --- a/src/Artemis.UI.Avalonia/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs @@ -1,7 +1,7 @@ using Artemis.Core.Ninject; -using Artemis.UI.Avalonia.Ninject; -using Artemis.UI.Avalonia.Screens.Root.ViewModels; -using Artemis.UI.Avalonia.Shared.Ninject; +using Artemis.UI.Ninject; +using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Shared.Ninject; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -11,7 +11,7 @@ using Ninject; using ReactiveUI; using Splat.Ninject; -namespace Artemis.UI.Avalonia +namespace Artemis.UI.Windows { public class App : Application { diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj new file mode 100644 index 000000000..c5b5112bc --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -0,0 +1,22 @@ + + + WinExe + net5.0-windows + enable + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/avalonia-logo.ico b/src/Avalonia/Artemis.UI.Windows/Assets/avalonia-logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8d49ff9b94e52778f5324a1b87dd443a698b57 GIT binary patch literal 176111 zcmeDk2S5|a7VMr~?`&u9?L6D5r)O_~sMvcwdp~TLayYQR=&jt)Ayzj1|ar_qzjj>}3?t6{b(C9x>L&LzJ@V=g=#=Jw20UVfL=lL2M z{~gmTy4MTV(6|w$snH9bK-Q3=ARS!FJP09mMIup;tn{o!d3kv!@^W%);OYRUBb<*& zUfu&ZAL5yllVg^Vkuh1CsZc0vmX(z?KQA};38YPiymH{ogEL>rnVXlJ_Y}Vu#0Z*Z zXJ>DO??NCgJkKTk#1sX_t1C|tlgWFD>4bgc>I_5jV7WPYGW!B~zVr%7^rTZC!#6?c{Pd1_ zIeFIbAU9KzPF`JjK#u*jl^h+ik(i9#LoR9kM=p*!K(3EAAonMnB2VL3`nrudy<`&NuX{dHBmskjyxgulTMQtE3Ohgoyr@s&2vplOB)Vp ze6g71$V6hl<|45ib%@-XX-qtiKP3U?F2rNcJ@R0RF?IT1cn$exVcoK!f1QW^)&ly{ z3AmSFJ)>R+k*BM#kUJAklKT@+kp}>?{lwGck?uL-bKHT5m?`)zmK~l0c(=2&s|j@& z40U)+@FF@h54?V(MG?laiB_b6A`x{u%op_95uY zW1-*KL%t#^|D0TsDM}~l+*GQ*ST{KEPYl%i81@@|ef=8vJsu$;A$77+Q~Lrw4nRI0 zkd6gsDx7I>3SrDdK|i|(V{0j!&2A<8Z9xti8u*N)q%=z9^M8XrJqz;M2Ito zsTq@?%nl@y)Rm@J$CU}0xWj1xrz(d5Byxx8h6%MGM1z`VI>EECaN>OQtsQ_HOm0cSY;4uk#> zo|l~y0eFvtdp3OIm6MrmoGM5ilg>+Tr>sqgfkBP<`1ty1DQRu8g=s@zE?JSAlluzF zpgK9!tx^Z%g3O-fKBfwplZ21Pz=E=#)4O4lkeR48$b^**d-mC0@{*Wv!9}3Zo ziHWHPYh@S2HI&U)Rxr-H(LQ0s{fZ-bcFdMI9JkdK4TP>??!5IIGk1zkwgp1W|T+_51`MJ_tvk-iShpsgTd>mb@2Efo5{(cTgmBR z{}7YmJITfI2Z`&y_o#Up=Vs~o;l#5NS;82hVfpYvGaUMxAXzXlJ1g6!L_&BVWb=vn z!XxC+pfyU%HvMxqF&nX$T!ZykTCVh3M)|dQ3A}dcsp)e8c4{Gzt%H~=B&W1?t5o*I zkq5|)F^5$uAMhVi2!BI~Kr$dVJJ(jWT>K4bh{cNIDwl0B>R)0t_NYqbOWR+KFG?15 z%ScOG1!W^$SnRkkSHA?lEoK}cyqM24jP!#vu9!SsV?k`j9WPP7&oM`7vZ5=LAC2uV zWTgzs$;vua^a6e$y(eIC$+f@F6zk__{@g*>Vezs_i~Ytr*lB<6_tO67vFl#3ba(^f zkB#Mv`QpEFv$CzE3DN|q#Ac5kef1?@Ouk+=4?S`1yyT@$Gr}xip#5tH0^%66L>Gezin; zXnz5gpPC{V2Xr3NXw>oHkw;PaNVf(&dQZ(QIKJPTm7GVU-$}30j-N`D|H;fn`nu=} z{XY`R7jyU{VcxkeeUUDbkau@p5r;E2gcFrWZq7F%(z(Tc^+jnirB|P04kgNuxa(6Q zJo{S$m#jldZ~}3hcdI46`{ib5Un-f95SJtOsd-Jd>^tL65W5LR zC18~;7k|5LvnBbkUdtc(Ik^r<*J1hau9hTO(mG9)HWkKXiHR*2*7Rqat`)(pYS}NA zS&~cvvKS?fv<#7CNd}+ap|E^S!eaddbWh)$?Ci58Qp1B>T>FndA*z=BX7@dk1w4|X zBR4nqep-rfFpo}ejOF72>1v9_;uc7g0&V(1(RcWap?n&G>|mIOkp}~psiRi zHUVd?aeP91i~~A#KCBn|Fn?c$b<-Z!?gzk2T?JWyA0Xs0TftV%!Ss)N}l7JjS#1jpNwR;TWh{xfL5WqSHeYXqDr!BFM zQM|JXFk@Thf`}j!P9dC3INjkiC_JGe*lra*F(3DWvnEqRqb`)u1j_0NWsU(c1wnZz zh*&jN!1*o8DWKX_d1&Hz#r})3SmcvF2B5|c&LgQnU!)|a^aMJ4WGYuM3*ejr@Xt zFpBf@bK!S3Ji~WNPfQ0V9+_~az?|crCKRucBnt+J6B1e=3<~O}^bs*2HDcUi>Lt;W ze!;dD@`RJ2&Ie(fzk~dX1l&-ksyxzREj}hlP9A`GS6W%Q7dY3@VO>X>66t!Vw*j0id=OdKwG7!3B;>J@yXrfs#)R|YM}{dZB_*9XF&qzcc71!0v+NB&q@++RafN_ zIRlU2!e=G_RieT&58xwBx)Z%_a!hh-n1}yNOHDHX*m)%~`w9=B9$XmXdNS25_LHhR zon9B|F_8a^&d$iTfM+G-0AHc%RFP2sd_If2q*$e8Zg6aK7@St(Wd5k!tXyQWziNL` z&`!BHzsgj(=qIGD3G-ufkZTXkOiwq1`&DqZ*kQfne@!;WM3OBYLa!#&Q=WgdV|LUZ*e z_x4(l3DZ%q80wmfel$b9%O3CB&2dz_D_cMjE*mHmGBIif!Avg6( z%EMHye;(AI!(S}h{!q6XLRgzoZUS_ulcKuHJ_9)y=r8Y*g9BHWyY48@H3zweY<=Z_ zm)8DJ4~Zm2v`Du8us+qrH38`8&F~)Accn*GdM3HK>1-wHzMowB>tN;T&zBU{A9*9B zi>Ub~C-oz;hDp_}wIUQ14{RzyMNij*CBm(hDs9&jV?|kvG8tVQp=$!vk zTm6%1w1y}zM%h_uZJ*3wkwZh)mao5$+)K&Y%tvCMDUkJ{zWmx~eYMmd>Z^$~rGzJ( z1Z`ice8YN&d6{*;F!2EKyz+vi&{>px2=!7T7M}#$dlB1t#+0rf>yC0W`7tYdU)uPE zdZqy{OU*yN7QVFw*mp#d#Q=-azQc)5EVJ(SHkgxikh3d0aBX{U>_FAsYRr+!)IUSQ z6;bp9&1^OUW+aL1D0{Vbj zf1{(Ln{e6OVZevnm*y|M0=Goz9`nF90%?LPOO8`|6Zv)3WaKWwk1ch*mu5*_QSSI? z{)JN8Kg`yv*f(-FIbyDuqJLNs5kI560_gg;vT15$Es9Agwf-MZl}ZBS0bRcm>yP{i$c(1s=j0Vr z9?;zVi`5S154zENu35f2>ySh*S( zzs;0n?&h*sD5BON8bmWJEUX3CqK$vTOrU3wCZKev>#q}< zwI^k_iux@2LqGC%-+l66Qh{xyY+sT8t;nW9rV7}v_+o*0dP-bMTdY4GEML}7{CM_n zKm(z?LFs|=gBw!~DSfwWyCW?ot-I~`@4rSJ2OsF3 zg4zQPKo7!==l+_?6V3+sO0^G*^Nu7}$LLe^yL`J>rtSz!m`$lP4^}@9+K_lKCUv?IjMtB3|xN4sO)(LSOqX)yHfPpM#+g7R3XUoo8>@{pA5 zgrB+qa3CzL{`fBRz7M%Q-jM3=m2G#NZ;-|ei)YLfdm-Y|a-XC3TYR_tJVx zuaLe_*UsrG;fof-cgh!WY37Ajq{m`k(2RrP>6WI?~# zo66?*L;WdySE{}k-q%2VIv>)5z1nuTFJZf=O4)hYxlm6b5y$Z;S*I%BC`gl=nVES` z#N`e}It|{Js^gczLrs)tfu3n#mLy|8v_XYnP*9)pJjwwc;S$I>N3wy(D!0B4#sbRG z1&PT6!FFsrz@R#VTb^1fNDF191C4N6%n^@3Jp>Kp%8;zoej{yr*((9P9pZtqyB0|n z$@2&bimvn{;A7)5Q`5I1O`HmHsfyNJ3I|jO?AB8napE{#VP2Y)ot}9C+DEA!bwvSy zJhMOs;t2X}Jzi2$pV*)RJvG#$-0d!{yYvcms)1_;^2%rr4RhH%u#>ZcG6fZ_Z_#)8 z`58ddIHPVF`pciZL|%KeeJjfzM_M;kuTUOkFM_yWMYB2pt?>uYfit0>o(2BKAKt4x z#sTh3)E}dLCg@}r;TT056Vyppw!f4G55j?S0m6YaEa>9u#p2ipSkQdhky zk`MAgXcL8NJL>;%?1@>dpK;#C6Xo0SwD{&oU!e^J!mX}4Lq2c-4*5m7v4*+28H+XSA1MF(@3#Vg;)9VrT6Yo4Gmc3 zrB^221E(RqQg8z2M8Vy$usz^Pwa*yjXW`I?s{w!mH^d#X!z+B)1h3G5lz|p80NacL zf3mUg2_y&bJHg){$B!1M+7>{4E6#gp`-t-(i^1xMHU}IgX9U>4O$HiZrija50yd`q zr1C@tU`Kuh^vVz5yq6(Pznzhqb~!yY%`81F{XDFHre&U~nWpfqsYH}&n#vcQPvr~D zAWw@lN!m4uP<%X-0N|~Axn=%+>SyKB>HMatcH&O#_icsgnqbIZj+Oj{$oyG~1 zh4a8J>}XDA)-zb|B4BMsJEPJWVw^VBcR-POEbwc-%1`2Jr$ndpi1v+c0@b@2`d}cB#nZw%mj+*H?+|wE>j@z5 z;Ke5O5hd|;V9cHexW5=5SDD5EfAC9SKR2i}7?r()a&dn9DLx|pS6%{pcp5)-BeZEy zW$N>#zXiL4IDS&HjxrdPJx9I=`#YP-?u;~gra0XM`bjls8|L@WUXuxrM9de%o84(539=gDy^nN!7{ zfUJq6#3T`>ZzQ3=3n3hO0!ia5pGqVwA*Fvp9aL#&VLX&FD+NC4M)L5=-D@IUgL0*m z_>{3gzuA?UX(f1jAho4620L1t)u!abZP#LU zKVF9+W(??p$~rl|s=2@c{3qq$Eq04BEl^efbj@J!T!n1R*??E;?H7ptkad)e z7REtP20PjjV_XE|;RQAzXTg5OdWkWKat|izhCf{>K2Z!{nHxZ(ChA?2qvN}y&kev{ zZr=cmzwki+I{9z#XPd_I!j3-FXpf9~b$h+DW#S(DhN}2a7pIp7e=U@agB{NVnCpw# zj+D~Hi(W;(4<<)PZz2E6*e_QGcJq<@6vik}G!|5bU!oX(#63mhM6-X(5KH#KeYyJm zJBasjXz&`f!jAsjHlVv#1h4!vRpHM_R{}riW>T2UHpnXiT}vxMstP}x&f0%ffgf>?a$B@Hg;)Y;vy-m^*i;gX`%zV}V+;dZ@Sj%%ul%#hz>jnu z%7a0sJppvusCQ85U=;9$s^JG%DH{uJT+$!e8ClkaC&+9@05{ErE$&3GN z$ioeniREN{Ds}~qcPZWxcC?AVJKO(*_G6m&ys=%Kvl#pX%wiemWtFp#j znSPiAKJp|Ot3>`lyMj2c2=Zj(6{^omVW;fpsu+Hn9jy-fnTXi@ML_QqcSe)1XyLu< z<)`I>{UyXd!gyP%91)IwW68} z<{79=)4sc0s?E2;B9j7Q$gPQnR2-9gRSZAMgh9_a6m;h$d=(T`PQ>A>4EvKk*U`C3 zQ8r~hi*WEGx5pY1b;F-7krd;9P**|mdD%JW!>sUtaZ&6!J2HV>TX|X`A1CEyOh@k_ zVs<4=5unKDU~_5*@lqA7ck<6v?f;+~IVHpLXvENBT8lWmDImk87Xwn}CMhzGJUVfU zSnn|-9#&2S{V34i#A=O6F&Qh!C zj1{a1UioLSFN?XFD9sl7|5aJ|(bfd?le3}!mk>g67>UL3E`=Sh7grW67q3s-mylhY zAGwE$7p$}r<#?eePMAFGcpqu+t5U8Izwnkk{21#4=C~5}!QvEwQuuFdHKEFTe)LW; zxs6nIf$^5rakyi=G8N=syl{oXw?q|E1tL>f_)#B*dP^Ap3hi2D{e5KdAM6a`U|1Kf z%{fl_{$QV%!j5tqL7c+uO4O&U2N;`775HW1d6$|cKbgB%7XG;KxV9qDYXJUB1Z%`~ zPXe-8^W{g1`T@>`u2-K@WrV*f@OzSn9pyH`4=WuGi?X#<1$K<%2jjO?xTPcZx zVYa3FLrUAmX%U73Df<9eGC)@i(e6JVXak0EQ$WV=Tv`s;4%o)Xzqpz_BBrDED1}|h z$01Ks(IZQoL7wWB?$0WP-}g-EzD?3Pz!+!YTK^e(4UO=3;A_TY4a&~Sx-Lyu+BG{p zi<}?5w@lbkc40f~3`t8Vv8}(e^Kq zk=Rqj6a1r6CXndyu4~2SI%%Jm;vHd^@~}_-zFe+0fI1TN9g*U;tm~tx=U~$T)kL)r zAI}bX9a;F%?y+zUz%{T04Wy_|qTGUu@m};xl!YJdcoM)@u8;>(ZPJGRd4IJT_{|l}b&BvVg&kuoBOh1b zLwAFqk2mgpkinNwelFrzE{Syp9}$DcN@GjUVkQ&ud=qDBGxcCam;h4ij0{P0^7 zZPqyPoc`czcd{sb89x#O7)oVUieR^eSj#BOLwOd)L&bd=l)MRVPkf*toS;j2jRA}m1LF!<~g-4vkp&@ZzD{4fS$z?UK( z^q!#mS`MF-6jEYF3J!51pV&+@Dw5a9j`ynQ^SFOXoJ;w5Yw-Cp1%37)v~|b%P9A=| zN4+=7g1}#L6vQ}JeNu%s!M#%MOg~M@>!fpCRltrueyZ|$QdFVM7uv7jyoa)0MX*!w zgB}2FKG5Dp#G!QG=I3n_F&D8?m(JGkh9#1zViSLz)sHEV^U-MDloy<%gh`zkI zSBNtBsWt#z0L}y4c=j-`6)e^7TD~B>M;ny)M<1(wo`1dO2JG2Wb{qitIsr}Z#ba@% zAdd%n4#d6G`$1tdfa?Hd--&k2s0RjJpr3r6s@$hQ91GWNHkDrEo-MpgVqU-=PAc+t zvULMmjt{Z3R-^!Ji@IH9<6gcYD7$7iT0`{v0l%{4Fn3m%k;gaz-bbDi?7OP2={Uxb z2EACi z!kzMINBD3LEX1$dRvY4}|A+)!a3)OHlMCa8SMmVo;4E9DXPKeQHfYOLR==0;1DL+R zmm#VpFM;zX_$QozI;o@=u4LUS{W;jHy+Au}Mku4BC(P%NVX0$Y0qoQx{0?osQ=kn& z&OIs<{4$^)s7I(*X($zEfd2SkuQ-(x%jtq+9#WM$-z$S%`W)LJ24cD3{F%&39tFN7 z$Ds{Wri~QWvPz!99ub*OMag^}PF!49^l?DFwiJ%aTyZ``83FbK4vqz(WIxDJs*Sxr z;3E^(>Z?MK;h}xHI$@W#8}%(P`7y>mgm!oUeUejII2C*^0ejRp0QYtwhdUAMHTpya zMzzGfLDV(R6#=K>52Tf`Y~-60!V+3wJ0Puqx>S&n9|1hQ0ooDULN&#N9MFJk5%{fr zg7{9CL@A<$;8QpTWp^9~qZO`g>h#xE5oCqQpxOoP0OJqZqABv3Xh$iCO9uac!3^+| zS`R*mmtfD0XJ}gp{UaK9Qrt@*nL6|GS?~<^f((ZD&JZ)rN+Oo*Nd=!ev_r=Emd#{# z#x_RTO?81=L1Snle~Due=V7F~(Y63>$&+Ie2B04-z%yQy%+mrLgsy-sn1LtiBhV*{ zo5-Dr<0vJTHJBU2>cxY0#F$QKlZ-Sh_BCv41?5(|M_5m63&a(!n>663VuOO3CHd2T zNsftWoe~$<7Uxeqv5k;UXXAK=HbY-4q+2nDE#y<dM++sV6e(A&4oBHVm`4)zXi75>h@ zZLmjh`@okzo&8>Tb_;X<)FaR}uxI$Yz@CAwAA5#+1azml`E`qY8`LG-Bd~LrS6HVo zuYgVr|Im(jhQ6=r(;v!!)5X7IfSXq*tY?t($1c83@4E(i_;jYd^X-5z1ipOVGRX05 zGlUUkxk!%}%1FKmKBAHx5M?zKZ;HGGz+Ug&lXs0gUwAf093y^XKSp+m@r~%k@C)xB z8x%D-A&mKFTqt97EG>FMOkk82!;d~K+Anez<5T#&n81hy@N7X`aMb)*8e=XqBzksS zXpCMQjXonbj4>@CELJxuGGS^$B$GOmqT_Ycen!OW#78i7;zOf#q5~qQM*BuirGE&S z7Vb@(5#<}97ZVVn#|WfPkEb!U5r){1sF8^@=0HYZcu&xMxASrKY2oYO`xCau7m$@z z5`7i=oEqb91_rfodwU`jYFgiOkD+{!<9PIGhM)rh&o}9HfQymna)t2b7p#mGwfUH4DqU6z>mR2B1m;7#|Sh1Xie} ztI}DHBlvLo zf-EW)(3$>ifR-Z9@A%YaQiB>&6VF4@wAUj!&Y;&Phq&tOeP$DU3;1*l#oja9yo+ zm{r-60QOXfnfI6z&03ro#vBn75Y`FXtx(qX&GZ4pJJN8tmeD+E&1vsw9pVC``o+=W zMp1KmEN5++NOA+lcNmQ7|66=3>q{^nFkm0zt?^+oW03~(ef_!#wkPT|R33a^J|VTX z<9vm9$2APsbl87qAgpbZQka~@Vjlk##Noree^Zsg{^NN;3&6U^bf--vK zjlMiu%PtXWtciQlpk4sSdl<}HNXxLoDFMApwix;Kk)y#1<>aW;y!F* z2GRdSelZ4kr0T>M;CzIA(#grGZonhArcob)+sDu%2Otdtc>f#dZfoer6}Hd&+!Fu4 zzd+|2o){IkAUY`ex7fEq&5)jg5&6~E0qloJm(W0Vf&3fYpVkLxx^cj(EeBfmKIqof z<6!*%i~1tSVV_VdTtiWAg*M<{c@Ch)yxR@8ddSBy1H(JVgvJa99(E4I-9HKAJ+7$Y zKYpmC1)xm@z!$G%gfM;&!Z`re+Ok(=^``(}sMyLB5AQ~6jWj)r9ycX9p1lS5w|DS9 zPb~od$fQIIzgf$Lz5W^bCHh&dcMkS z>q<1p|JeiBH-^TFjGr9_GBcE$mX0m8z6Dz$QUm9Elut(oM0dx2$YZ6fE*$gUt6Z*n z^)Rrb=EShp(Y- zWB5gk3U>Bxr5t5ydsCpB16fY!8{al4$6-as&w}~>Cd~Jl-+yaYKL|m$Kb5s{W1Deq;9}-uTE-3 zc=60ASsrDR;2qd5o)$BV!(c4|xvlG00{cg?g)IO&WN*5E_;j=-Uwo3q+PI4T370MsKKIA`YfGr^A zi=85ULZ|vad*4xQBfc;r$i4>37DIhQ+r)oj3{8qjSPtVp=ts*}pB4c7r$-T9LE6DD zKd6=dL)i}6-=Wh0LktVLj}q%_`c^=Xm+ubMz?z=vTzAyWdKyxXa3{6hW}iz zQh8!~MnL2wFO~kF zb);bbhVtVc<6cW+U!N)5zsh{lLGtRj9Z7)rfEXWG_Neyw7o^%Vf*FASh)Uxh*L;-g zqFo6yIBG;PGifu}o0LC@m23l6@HZ3U|LNj1`^3=LN{@e>_tB>cb$RT_SY61s zQhO+tci4-P1?2v}S7BeS)dhPLeMQ{kK7JP<9z4c`x0-ykdgDJe-5%|LDl`8Bt~Ak( zkdo_zJvQJ1_fz{KYd+HOxG&Y=ksGTW?#&=p@-^7gN)@_J)imm+|G=)UviR3TJ94z! ziVtS=XEUjJKeD{zw<7659SqecfZ9qg?rp11NR3| z1+S{6sV?}(SZ^rji-)n#iDK!Y51xV{tF}i^PuhHQxJUfsUNEZSR+V(s1pkz*Ckobm z)aeUYyd7Y|Rb{cVoi9E9CUKAZ8h?-YM>#Lj{OEVjrYBAZn{79>4RpDT{GPu5W^sSz zJH!d_^)2N9H~rKD%(N+UfH;p?uGe1;lE(+_pFcp+3e^9UEulLDvo8v zU!siX^0H&!1@5nb?Em(6H2zWEhrYUu;PCz!lL2Hhe8pI-_*1_p@4h(hujn2oPXF1E z4>w&%#H#?p^o}5LA0i3kEsfBgejxA7oyg-YSBS;fLzGNcm2r=_zdqUk_Qv~u=6{^~ zk>|&`U(6Gpt~izze~B_alj#S(i2mLT_VIQ<_k?i5RVQC?e|!4tK;pRLMhQ9}X+7zj zFU38|{;bCtelP34Ci-iKLaf^)h)D|jFTGNX#fm@unJO|5RKqh{+Wf8aFykka|H` zTU7M9!*S~>v(@}?%cY{#Qu(_~Q95y0ccp0DBkpluY}@Z+{1>eK5Pv=?Dqb7o>Z;r@ zDkOyc*U9m*+euZ}XurGkOobY#CkgB~PaZ8X1H2dD9(jM<6I@l5P zbR+oWZ6M|G$5Z5!MRW31cNJC74gc z<^KN^?GO7bVBGLDaoY8AHH2K^jMOv|@ji(7K7C7Q?*0V!FPBRJF)3g!xG?SCGW~EB z;U4|*XwN>D$n$GFc(uu@+K>M?Ig?K^l9Z zG~AyB{0Ba$ULj^27hO~<{yp{8YiKNi+f^Cs>^T}U4`)fRx17X@)peh;O7UrA6)-GFV1DuM9AS2u4JcInHySBoyzkg^8QD);ve%<=ON|_9z=YkY5O|7Q_BC#(Eqa`rc-cv%C}g1 zvRwE-I$;aR$;>V)!tLxMf@8k4aWBO^#@k7ut2aJkQAH~F!~fhXwc?-Qq~4HPuutyI zX#a=_{;&MoDx?1j`2WZ*xX&v1<@lASDL}SZE*=2o!qNlujKos!sLHrU`}~L({?gB@ z#no+_dilS2kI(WEbpR-W{lXdkk)uo5{{#2us)zfovLh**|99mr7wN#ggI1I|4>+3K zDVBBcQ}1%&9^>t}o%~EY6wB-@+@QViLoE}vj(-82qgF^nDS{(0;KPlvdXz8wL z`iUyD^D8gh2_6w@#l8Kc(**mJIuBk#%0IDTQG-j{{|WVf7{fg&JYgKf(5)~7f;!n~ z-*Dn=`Gh<%x=oPIM;)N-dXKQ>X6KMQYcG@=_ZV*neKX>`BGlPL70&DZp@(Y4|Feac zD_j>vAA${UdNPx}3gfoXA$FsZ@vnh){}LM#HB!js8!5_5UC+`55@NT}yu!Fg ze>}&3Zm6p|70yQ-$0H9WpHVCR-yM8V;rb~05bU87F>=zAwe=A@f7s=*R+20Y)0mO2qVYz9&()@7mFS|hUAU5dN zIFbWm39i+u%5+psCx}$(zBRi(;G(`12PnbYDcYRCQ4nHSW)O&;#Mm_&~s8~P@+4bphZ#y>o#;`<^G zm;kYzVIP;`h8jv+L$w#M2Nf|LwYOY!zM?rFb3hoaDn&x5BK6j-j9HPS1I_{z}W8CPm;po$EFD-U|6r-1MPNHRdMm3Sw|wv|^EvKVCAdfYz*14uX#uI1><@B8|76ZG#a1OGK~ zujaUr=s$P~?CtQqTAk^lLC!DLezqQJ9rs1Jm+{AYvg9I3^oc5^WmJ2Wot8y{uf9>c zd{=hde&1z~ zweUHytfk;{-;eI?-56u}mWFrfJB$Hv6kdxFQETD`e4hBds*D0Z{}U_&$v6`B)JE6`e>_fH{leyKk?KT8Qb#s zmcOrxbsu{Q?8$eM6%qRv4dOYJ!S_p1FTGMR->Ln4!(zsyrijj#uji?jI@&F`+;O(P zH{3fdvQWFO4_hDTb~YF0{%DZpVk|eD)1}B&<%;QXZ%>AQ#P8f#hyjblmPgI~uy>a#c$cPuyeNMVnsTi<kyt zWnOkU?ZV3gAk>{a|Hn#Ue7$d-$D?>YuoZ|=vt74*`=;_!&nJX4$D_O#7R&*2t9rlhZ0G|owp)ES>pjl-(GH)aXsW7f zeySkV6vqBI9Q%N?`cP1%#=f*aU_Nx1147^Uwn5uaej;}-OaYly1qkMgdO{C_2j8?@ z563;qcH`aE>&v02-C@iOu+9RE)K6O}2xwJzi+l`>EkrJejuVh5u=Im#Fn^+k0*V+SzF!#So!x}54R&&P59{@;frOPrzZrcjt4?Ct)94SF8* z-3^B^ieptCf9kkLIReHA34I^hF(D#$0{9e~LWQb~7L)}xgC`+x4IV)PPk@hLEu<~cJ_u~eXRF&rR2Juo zesjP+&S|A(wbbKzA9+eL`RcdfP}C0i4CehDNwV++(tH@#4aa6hWqqpl1t=D1L3-U_ z@8DKwBgkg3R>L|FudI$$@jMseh)04R-(mj6YN5k@yWgI0LlUb3)Kc?IPfdG-`?4|u z!+WBR567mec&to1Twf?Z0q`e?2RmVq3;tKt{D7i{KzR|vF_64ie)Ws%@sX$VGI&h* zYWCGo1gD~BD2GE{A8mAyCd1gBkWMZ9o(g?K6Zs45bGR=!>IWqv2?k{RLaScM7C}6q z4VA-evnuTiXah>KdQZ~WYITh&2~a6da6h)>c=ndWFy@Fj|M0e+o}Trqdfu1s6Hq;h z9|!|makMc&eG}{#QSO)lrGQzXSGYEyeHYqnx^A|vv~MQ*ajkG*O& z;Czc~KGNa71-kt&HSf!J0Sy3@;t3->KmE!KV*Z)JWUU6<`>HW&sj61}HuBAfm<;#O z9uxQH2=no2@rBp?61XpXfa^d_H}ET`y`y!AqcKKt6S4%2Ih$GZUc zV?;ZgLR-#ig?njd1AxG0(4scoo8FiSv?=^^?oU{12_qxGaXF6&xoGf)%tQk1?3Y*ORFCq>BS?+2Pdy@4*iJ#>GrF)B|a z8Lk+oBQ7Ft6x}ztYmj45Ga8LnryB8iWuaTydrbhe2J)*kPg-;IDMh*v?MHyG!S$d@ z?8!ejZuR~JvV0Njvwa@@D^P|S4DnZyc0zwmsCl(t>y;s0{yFwzU*7_{FzQ28dxRBb zS>U630(iu@>W!r;sa+n#*!Jau9)}gin2h!m!Op?0aIVC46ZvWRHvHD_DH#Fk4FSd| zplc%if_tOwLQ+h^a^Q9BK-m6&K^Nras4&W1gVLALR*94gB#TluRJHQJqV}g$cGZ^Br0&j;j zGRjT9r}2t^1R?{dJy)X^oBI$3))g8({$xCh5jr` zm!v!JSSf2@&C^1j9{YVX^nV=l-x_bH5TO-&s41ljP>+qWZQOq#O(8l>&&?P`$%|`#IRH5inQcTivR(QM?%05sh5&`oZ z(7-?8T>l;LbrnUo((jNy#JLdr0m=?hW`J=E(atnJJVuMdU@ZY#!^1EqxWZa0RB;%7 ziDdc6!+<^JL+q^y;C{+sK4C48=0%>a5bxhJeWw(^s=lD+gSmD!X<*Av z(MB+C(B}Z-n8nhf|H7D7AS+oe<_nELKlTD-Nzq?=je!0q$j;0T$Vg4ML75#pI&mR~ z&YB!gV+_T)D;(_d&^`xcw9@Vgabbs`Bf*3d2 zxL+l#vFlQ~qH_?Z<|WL(!4OX%3HpROd=w$8JTdy#g0I5|h^J}CWpn>USsf>eH8UY1 zVFKcu+FJp19Z1U}Era|G&Sfz9{21%SP+GAYYHffy0q0N$(1TLEBghbIoD4UT;oUdW8 z%%|z85^G@!{}eYqc?&l#X+?4jdp`O-qTMCxv#|f+ehR4DK%ArP3(7cujP;7)w>)3r zn6jh#f<o(B6%4}cg*!?0hl!xP5izE)^E$z~)@!#(bB z{9<20IVAW|%)oU8_esEbxfuVPvabd?Wjxq7qrD}{X%OELZV^9Y|HCyMZSate|1rOp z2ZL%&ORW*o{y@oB{NsssLa`y(s@AGAD@q5|q@m@B2yqBphRUT9Bd-pQ#4dmX- z-a`Jxsss1Ms-xh(SoPq2vFa(fXUdi5UdFwF+;7i zgR4=uy!XYMN26|e@0oJ&RrS5QTzTKxeO0$lIq}y-E`1ZZ{!`X{N4fMJ<@%@m{TR9T zW90h3{Jp;1_>sRaHaSAqkh=#{yJ8(g{vPJ%VhDlzVhticy}@)_3}E^Dj&jrG7_jb! zS`{5|Uko69xqG;ko$b+5P!<4cIbjy%2D1wsG8LxoWh#iPgY1Mk2JdAmq>uM96{2oG z7f2N^(?W%-Sy6#h_A&)@Ecm{t0R4h{DMW?Y6=hhMT~U)3W>-{0>F0$_Q1p4>2Sv%D z6l{{h!l(_6A?yl9)=%k@N zaon7JSGg|x_q7Y#&B|juxcD%6>#o1+)<1u1qE3x&3mg2l@bqsBFY>&nHeaW^e|htf zKHp7TRkEX>*5L>^pzveE1V>uh)=%(!z^tHeJ#h+r0a!kz;Gr zYCHCK+Ow#GNlmtEwrbkxQ{M${N#n-2+)TM{_b?~)-p+tgtxg&#Jr=G!H~H75v*%9F z8I^W*%8CX~S9AZ`dvV*1bVl@x#W!-_w7GL?be{3$(5@{FLgU|Uv$a|`a`)Esv`?Qj zGz~mND{E+U{q>;FZjH&Qw`(1(={>z&5BKiwbvpgIKjBP_>p5QNzS9Gjq~#9hw@n%~GWGBn z7c??xYW5@cow_}`f86zINN}5han1T)7}`}gwbrv2J+AzEky+!&(T!HM_2>s z*@g$ht;}ZXSOgh18{g}WVW086W}a`}v__2@o}J9Qof*`qx8_d|tp1?sYPa8@bFblq z2fYFZHGJ0GaH3}5d7U=e_pBSPskPPc=@QeX^^9yym)GxlYkk7O{YLeg{X226j#k^m z#K6GY8#}hE-SEDZl_pfXU4!vQjcVU?Z92B=Ua{-74gz72+2+V~&JRZU%z6_WV%VwO zkpaKm?Q(o&oAIre7;LO*`0EmPjlkn8j%zqtwfV)Y`8ccEt^YP_KHZUJ*wmw;>CabR z=(et7W^Fd}sNNg%IxEc<&FXuj#!sDej?bKQc%#jllUl9b=>77h7gnyvh#;LQI`_1C zw{LDUzVkX}LkL_FQT)lsPIo>Dau!*zYi4pxKcAMofXV)$>?S*Yk zd)8hVI>9@UAKLS()#WQK8jR&x@FXvPTfKh0LBqy-&TF2TZSC&s=wo4?*xPFV`khBN zt?g#GMsM7-4@TNA1{~A8&|+)zpBh}fVmA1~%#E{dOwf4x$z=4w&s4$I*TEDJLp;b zJGs$~2+PDdACt5jU6xBwQ1Y7(~SpNM?EHiS>{JLR6 z>#den#~iO(Z`tu-^OnbkJ{ykgFl~CId%fcMkf2Uc4adS`BUXgWmyQuE*I$HTAX+vfoFt*%q z!O~~CW=jjs#GoO=pReQ?uDM*_)8cKt-A39=roY@2zV^AL(<8^tJG-{9zcDL$=z%*M zkIj44d{Us*2>CCKso7F$j|%k9ID zENVQ=V$>bM;vH#BMM^iTc29b2?wZ?m$#^3&1v&*$4Y4<9@?%1CS4 z@ma0LkLfpgoL>q0^wSpL%aNz;DE z8i=hI_n5fJc+1``x3hGww;i^1^};{8cH4XAp_BcFmv4I1Fj;Z2t8TaYYkGHHf2zUt z9rWgx{_LHU_9o%dOq~|S3{%?)U4J^V{p@_<_1@FwUl+aAS(deNa-Tn2)PAa$?sC4* zEo*HXuJ)t2X$QysW8ZpJo#qqHy%q61_P1!@9@qKh68_J_Vt6}hHw@E_2pM|Kddp95 ze`z_Rxrw8P@y~zki<}m5)M!pznr-j?x6h0)W*A)@_qItcleXiDSa;=9;b!*z-uK>Y zXul%z^sOU(rc4@hwf80QyS|y%1CFj@8eexFb@JGXf8N!2JfP=@(RDny%PbeSnR>3* zh9_s7+6V8^eXBKVs&&7&T7$>Y4LuGHZhvoBxXCjk|5q6UKaTCYRNOb#|DKV>l)jV5 zvjGk7IGWA2>gboba)I9A{s)3*Gjxy5d8hT~z6i6xgoGB&p&7OMX{7F)>~*$b*!rC- zx(6+!JDu#?sH-`%%d|f~4miCgs7=Q1t&8J&yd5&M2Gf{v_?MH7bGP4^FaB@nPDi1` z%`QE=_D=0{$~L#@;AK{Nb6n^5H)+-PU8nqyvmxt={WR9?xE0@|$&sbQ?!>1WT54IJ z?>|gCl6h;|<$izH3%${2UFI?GzUH>4A}!4l-4ART@lGQ_xV>H1;nx;uux?uUEVX1ZpxaRLs1ONQ~Pi^wdyGf7SvC}4-udw6pul@U(Q`y_C&i|fxGot=K@`GDDs+kFB9wLa8;bj$Y54x6Sef3o@a@Vp*PoSXCCzN)t`;fP6- zCGItD`mp@y?Dl)@?;L9V%hpSq>h)><=AWfrPQkvn7WSWb;@W_Nks~gQS@$~K&n)Og z{PTK?QoJ+X-0A7n!%blRHg9G3zuHdrnD~-8E$97RX6>09>}LrdgxOALIBqM$Y2nh7 z3y-zBrO|F@#<)iT%L006I_%sXsnhy+%G&0=k2>@lY~1+cfRVx1_`l9-lX1-~Q1`a6 z$-=gaHn}Ykb{KVFxc+kXloN*nM%qmnG_%ja;AO2_-U@QOG-mk0#RDRT_4$3sHs+D$ z8jYqoZDCA4QP(4MCdBEu5EGa)X?c+;83qQ zb$iq13|yX{AF?n1$>Zk6bN?ml^-lHZ``_^Px-%|$rrurQ+}GXxT$FRx8|~j*^=dKS z&DH-cRLr0Hs!>;qXDw{C7TE@_?P(l0?s~p^?N;rz*Y?iVna9<2zT9HsvM1vB+g)44r1yzkKS$)1J8Q_m zQSGO{wd`Y(Jji zuJo8SLjTF(ZA*7H+4`C{baRZWOW@EjL-Rfw4*f~j6>Ho!uMXO9|F+A=X|IjWaC*Et zW4OS4yM2B-gZp|=^p=kCN4(nn+p3erdgZ@JF7?pYJv ze*-e!1nM;HU^*B&b8FpJE_Zh&&{`cm6y7TOP)79mbLO3E^kuaiyJ?!!x!?K)nm3Jh zrt9psSwf$-uAdiw&*q)x=7Yj^KjLR<^+|EH{FFHVgYm-sO~+r}VAN=9pMU=O*=J^r z^&iqUuh!BQu3q$fl2Zpa{UytH(I@ro(*JSuS_TO_{ya6s{i<2l$WZ%dE!y@xf)Q+MaEdmo3m zt%|GhpudI}KjWZ<#Sr5;eayA?I@+A)Ogs^LY8icK&fw^KzmCj~)HnX8Wh>Ung+mOR z_KjT>W?}1*tF!%Qv*tQOG;A7One~*N?Emz3VEu+onhcAK>K^=I{@WHFuDjC>S1#Vu zCAP<`J&T`STj=QJ=sWk?{m7VWT0I!kwKiQ0G1<`g-{1GGbGqumo z*9i`uVlsKnm_OtG{xxCgZzCKY&%Gad&d+jKux7)@{&!bsUEibYrDM5e?TN?x9%)_K zbYTAO|Dqg?@3-BuUN`0MNYA-0DX;!pw|RB$X`42`nKgdVwDHG@qklWle}9^>@cF96 zj{7aQzHD}6o{XiDnPe;qn;5Pp0%kX&>6nxk?DZnISc6`?Ug5Y8xI|_ zc8!(whqK1I`Ah$Kl{;Rqag#}jE4b_b>GQ~WmWU}F7t?0@8^68JLIAJZPMQ>anI!8lP|3?nmc{yz;GA4ybJ$y_P@Ny$}HX1^G^PTjhnOYxISuf zU>QeiLBDAx9Ym*QkA44-=VF~&i?lRYd-ClqbS6Kv&{^A}{+*AG>|`esrxb@#TRQeM zduQ|f?~SKINAyXUN=6JEA-Xx=q;Z|@WP59~9y*D$Rtn~9G8%jOoyPP%`5k62Si9(@ z_Atw4{RIPcokj$kyABMdr9atdZ}@8qho**2uYY*8cc9KSk!kMUdZuP(?VHy!v)}Z0 z_{iFu>92kZ+53mS?#*A0Yx;!x9qsur*Jrw^?wu(6#MUlr-z0An_(w&CTI8?)%e1Gh zf77?ke?8{Ib8j$ZNZ_KuHoIDDx9m-u_k=boEb8cq=VR=jub6s!%e6~uGpJ^tvFpFV z56*MUET?s|?=W~}+Vj0O0e7E1_+JEe$ub4fp)5boU z-ibRT^P+V8tT(&>;==lx)0{WFq0Q&%FJ341+`N|+Z`d?&{m8#p`5)V0H1&l?7q{l^ zteSNjsI_3I*{<=Ok4nrp2HU#YHal$F3>L`_FL}*a*8P9EH`m9IvuKOY>RTUoouS=3 zvvc>sZLF|f-RIrroiSQi>)in3sr5S#KHO2amrfT}*CV}eAGtm+^jwG~!$o@=ECaD3 z1KaI3XXlF-zD}RV*rfGw`+^TT3wgF1ByCNeS-Je4W3+GX_0&6mC3e++Y!GC9_Ga_W z_7m&5)H5HyFK+(=(-n1&t=!qVzGiT2(@^e>*zMuHTWoP@JZq`Z#Sg!38DePQw7XkO ztJ*WK>$V)9SZh$1`Vq?q(jKPNFb>XuHHcULHd7_COvK1=NHXx-FPbAVzkj=O%?`2ufJpZgES92UKcc16YTO?WN zu#R6Pi)(2oHRDu*tQ$nww*L?tj2a@+eu^RG`5}docs6x zJTLa;IlD7EbM5S0Gy9cXUtyXerU$mrq5}I|fxOr6yc3H7#R^$?Ca%>zjZn6RE79)R z9rNwNTUIGqA$@2@g#H)TN0TK^N+r7_40s_o(ZvS?dt;Sx2?1>;Rp?yu08UP@a}o`I z!_O7UP1u@GmucqfuC=e7c5CaV-b#8u4(}4w`X_h(xkeX3wwkz-$BSG;NXnv%d10&e z=U$=8X?SCWVky_EnGdCe(;&e3eD~jS%+W<|=GO%5(U9mQtBzGr*4|Efrvf)6>0CLg zWUIrr>=sYQ=4G4xq>ZiImuXUPY}IUzspa|L`GHIEFGN7Ec^RJ|t8Xs!?Pebhjr_hy zGFzPsp5mBj?8kMDt@DfCD@C;hyvPy>7niXA?0nLAB$UXe9=%lYiyxavyQdLBB9+xp z7F2xH_D}tnst9L&(@zJogb3TOg;7|TfEU#x|toHuf8a{6E)6!%)%Kd~UGf#F9XzDV!@ zI9}6v-7OLT>6vEt*U#Igiy+?Soc<49iZAeC-qK zo^WS0H2*!F%_mDGQ!_6}e$~DP-lK2^$kA~!rXQo-zHF~9q$FpmPK}HLCn-p+k`g{l z*CEBSsv_!XeG7u;;$`npPkci9Win#!SV6#A5cOGj(y6s8@#55a!ji4PNLKA9v{yqFT%fC2h%|%wC*018&)Am1k z{HtMaP=WS$GQnpb2U74p-155bC{L2!h;OYy2_qQGaVW1(2Mnln!rUPrv+>183+FM& z*z7YM-^Sd~!-|oAQ&u3q|BS?OoA8N4{3~dwCWf`EY4fzcDLue#kkn}3luJ-VyOri6 z5`^Qq(+h<2z2+hNM9urStfF=4ydS4^H6jTFu5J*P1rkCLZ#rXxo+~1+SA_?FAXZbWi zW6g6T;pLUE%}9&VXdf={1v}s#jfET$ThZ|T$neTk$(Y`%#Q8eB_&?xUHdsNSYH+C~ z3PT^Pk9QkJGF;Gokyp!CBc;<#8mutUm}L0Qg#$j#JnT1z;c8FWa#|HvWTQL;tRJa2 zheTUDFQ^zR8fS+-3H%O3-qAY)xe<*OJrg7fDyQPo*zR`&+-l`0nkMX<`834fw~;Cm z)fxJKEmTrMo!rHsGI~gmR#B>BX%(bHio)`sslE~F@kZx~Q;u__o8JFwUE67Dy~C2P z&m(L#(DK*>5w-?AVob$l9C_?%L)(b|lk0lEhEPYOSTLa zhN8MaZJU({y}AQWxr&^e03V-xiX*Q;}5U>^)$G2}I_HD^-cC7m5>wa+DWBvY7){{t8cc9ch zGAkS_6fCJi5Bt1s84xF=L~7kkKRHrAyrigiD@!BZX>!-RVZFBYd46NkUug(YdHJ6J zS-E4kJ(DZiit948DT*1^u}cQF>pLvS^9uxlR4gRJA|KmObc$YO25CydGyj{QfHFLz zjVx%xr1&klRWiHD44y(9zpGQw!22s=OBsgu@1l_Jj=uLbwwD!^c*++hypTbL9fzul z`afvx7-dkBS{5I57rE=IT=d+{pHd?0A4G)W>M{4Gu(z?U0Kd9ru@qRdhrLgaXh|Qc z;inWpLN|UMz%oC~G87h6YOa85OC_p%O8vJyIYRrHWH$K*?p$|n2@}?e@7&DEML7?< za8%umS@Z)M4*-k0&y_o_j84rCs%&niWqa)(?oX8oXioiFT1_=RE>2#=MxRqq@Qg0O zRk!1#YnBbPb99e_?$G0?L|a7c@!aO)@RP@16*KbY=gh-4LuKB-`!-e@igK9(paJ%J zgMW@N^_u9+wKYc+*6|3^t=9T>R`UdV?#W1bHS=r>-|kWpMTxPj=m`ISX7h(@rO>$6 zlc@)JFDBhexe0PC+a=T>rOndNyQ#X7HDX}Yr=h@MuNwf0spYF}SxR&>aMhrcTGO-k z^ckJU%dus>f|dhFy?M9sRK$C-H`L;xqIHmxNc$=92z{Ce>mec>v!vzTZ1+vo;yQ2N z?SLLdLlPeC+dK<&_;D6PJiPiO6giX_)X=5DtXg_&*mUwh<2t6bWw%KzW7S_t19~Mb zr`g_4v_;br)2m}Xyt`E_E1V+H{j>ZJgb@!VuJu&ve!epq&p$lyGhkHsVm;8%^q~W> zDI0uH6`&y)1YGo&5ok-cci`V!&22TUvAMGEl*9j_!L?paYeGFB0(v9cy{^InDE4-Y znj8CU^4-i>gK;pb*WR2?{h9( z`GK!>0yq1KkAPn_@~%1B(7O4>zLwW>M|rl*1ag#_&W^x=v0n&DMWwm$Is^E9fawF5 zc$^@82P8I!%`szW8jfhwY{6lLc?SW_$>|4?`vu-h&>3w)H~(_x4-9RU^R7U$h|8T7 zD~=hWLF^3O+V;F`&2=~ANWJw&U;F=?uv5{TDOy|XPIJtiu-ZAFy zGn1JAMJ%waqH4LNChZZ-VbTQp&D5%oD3farRL>)U{?IRCqe?MNZq?e*n#kb2^87ru zg!f;wQky{nyD7VTlNi3B1jOj{uLx(LWcsANbrhUvLOd zB{R+o=%n{@XpYLY>;bX?zS=I~GPfTMRN1l*0nWK>T^i2*O5#>+1>&b4q&8F=@3KRS z0u$u(eCoqaN|`5Q7IrCfC;qfC`DZ^^-p(OkehdI<4M!=R_UeJ#-K!sq^OD)lhT65> zLcnGPr}Sq1!ApDK4*(QrWC;vZ1{0ut6ZgkGL3I-gRn#1ULzWDUcBjcry6pSQrIBC+ z-A_vouz+9TNnhVxncW%5k>?%Dsvq>p%~vTgS~4w2y@i{-t$*nzQUCv1fd0M`L;wLk zA-SDDnHahbEFLNzXPTXop%Q&d#-Hg9j{7lNuNPWhrLF{t-2u}>X#yp`gMFV zI}DwE;E%0}$%EzF%dRnPKW4xNHA2wH`D&ml;ecD%Fv=o^{%_0#_Y2k575D+!u&cmN z+A=p(a&YQ9iMHpKodyjXyro5KSchq;^W1W*c=~IS>*#CIButTaXs};p0IkHXAgn1? zWWCkRIT?or2a~84xiv#7uWPbA@c=jOQ?l5;@c1}vQk%jc@{%FHw8a2{1FZ3c9bkGCe`HfFG1){hEA|@YkKZreSDhbz4U8ap`FGT)P2Yzy93U z2e~ugFTYx3?r(PzD$ZegS9z&lAHsSI_I=)3&w65*XCcmqpsE9Cl^KiYUh=f|hM__kQtm7#Rkjse8aSeX*PaKipFk{l$`X=IH#8^*b=)rZ=L~bNv zP8+~(+@8X=A-zRFp10JVtsS74P#tzTxM|r>VbRUc%Bo<(#iI?pfW{ z@Oo9}JkMOfr<~o6lgl)24QE0Ze>l`^yy+E(R6S_z#&j~fPS9dx0>2}Wtk!2#0(^da zqoYnp@qqhqar&9vYX672_^3$XetsGbp*7eziPEVD9BzU6%Y#=$AU6-^BEm#?wu793 z7`%1B8uay`Zyt1kWmRPjIfk@5|DXLDyuFyVTCuk*^`!*7LTDi(j^njo{byI}Q43}; zET7ka2-4Nan-xt-=FfJ)o23kA@MSc(@i1RAS9-lJJ8L)5Ih6X>0ii|56x?X!;GK@l zb(c`kUcaJ?f88{+2@MwKHv(q`J$;X!4QptrN5vn{4ML-aHOLspU38&5T~{v1Jte}l zH7PCiC*4H<4JWH374SR;%o*~oUrQxry{z9Lkh@A=NOTAH@zj2PIp8!|onm)a*=>a| za$Ip~aHp7&1GtPOfq8i`3m65D>^LHX868#l9uFB`Wy^r)z&yddo@7%))v6fLw=7Nv z(2{NaP3e+GIZ_j+ronvhlm2}0JRa>$aqmVn)NCTp$WMm5ra*6kjGgVk{U=*Zx{^$B zhIz8EPY76x8)!V+d6~9QToOq;vw|8UnzWkB4A4MlT9GAzQV+1a6g0^ zvzD)(bMb6)5cG^~_S^CnHKRf`H(lOFuz2^$%~WZgvuC%<@+KOP~EW5_OJ9t>8{s8gN>TX!X|f(nvd^YA%B>5gu0fO`H|Wdvr);OAE60%f%I-_~ z4}4#D#K?#Q?)5mBy^tp&8aWlU)pS!ro!D!ES9Y@oKGIlRaJkJ8kN<>%lQpXveto+H zd)(p-wVn}ep05*Lk@CJ-B($ysG_uq#6zGE4>yEb8I(qEh8Fr@?m+bRBJL%Hip`Ra5 z4DHgf83E}#JVv^$LqXOT{PJNt8d=-KVF=v*eW4)YIad`l92zVd9(kB#Cw)txw|((9 zLI1039sYmgM@_R?O3*oR;fWtFvwQn1Pt08@3es=u*uwXCj*?ojwTn-;kD*bz#-#-p zt9V2qYeSs!HoPGX!=n(m8{UaqpwM<$oEWTFSBvb{KVIIYD|y9mrS*YU*p^p6Y4f3` zsUc4c@p|811m>G4I{%Q^t6~yh{kP@cK^h8Iba}x>hz)O+(;`FTC=Kb#UyxxL`=gHw zl2Oo5wN{|KSntSTW?2WAD9Tw0EL%+oKje5ig$4b0SX22wE4jq`tedWrMEr>wt12_* z7NzB-b~8SoE$&fTdDc_!EdIbaK=GDU!CRsh4p9zpT7} zsH2OqYKQd|<~%UHZ;cdkT~DDI4xHq+XG-+B@e1FUIf{8aOdP+DV$clweqPgt()dcnFy)54A%n+ z<}H|_?Dg|DxxmLxhge5d<}>d%E(_Rp!AT9ln?%IjsLK%t!0(tJe~ z4LT~XGe@Hh_BP13eA~-t#Z*;2d`URqUsibWeio;l`;+ek!AOWf5t5}WV#JtiCN8L~ zgRD9c=nMDshW;6D`&+#?H1GW@BGFIpmlx8l0t&Q{{u%E3>z;-k%_MftKHL^g2Lj7n zV0AR~!dUR@Calfkb8sFEc-W`gDJ6%qL*iivF|U5#td{#?SBd~lwa!Z1RHFHbK{LvH z)lHcHGBN5r=?hM|lzsBX;{vI>W%Cp$%(OD(8}Q`ex8K_f7pE*q(66Bh%btDeTD!>pVed?R5F3^$~`RZ%!6ubeokffb!oKf*>RaBxV$NNLMVU@6O4A$m8b1nY6 z&(8@i?{!b{+{XhA@nwNN-!;^9%e>0zK!M14QJ_x36Vy(U=2jMK1iCXCF#0_0EfDk< z+)~R|r8%EcH3INyW?cOR@jmz>rQvldLj~SB56{*NH|=b1fvr{M)3{-tCI@T6N>z=r zyi^}Q=cSfQ0Q-;s=M_`sk5IC8b%t>|wHzBtcxrbA^~4bPC;yYtKTmm5%D)(L??8tB zCS?DOo|iVXABz`SyR|z$qkQ27#Ui0-?}48CR{>WT>B|?voDiJzx=YpoTS{V>lD%1)KK)SvVKwQQXwblm!EpR#r~3 z!Z-}maF(JEs#Ef2?dgNB^>g@XcI`z;iZPy+Y%>`2ybTH_^E(+W`@NYrbHHN|mc8Sv zuoVH%((BF4)aHJ>cQ$;`#^dSH9)n$qmO*4c!e;_It6JHPnM~07jl-Xy@qjob1UI$u z3;>;^L|?2f=KR$v1YI>FY8olAJ5J<%)yTwbb~U*T1Dt`nFWVUb_4pF~&JmGxI;z`J zZxaljbTDj7HPE@ye~&oKhhy`uZ^y&Hft9mS;8Tl(N&2=`-FdG9Kp|hupdjdvnr4!v zGZ(M#_yZbL>WUEo<@)aWe$!}uI&RA!*l~PW@5&20$oc~W@&L;HM|NLr^%&xd9KZ>0 zsw8WN+440l44SYneAM#JrGew&h;guO&<5=_D+PlF<$@B)FeY$rmyM_*AJFvham*9Bo;!`SHG=AjbkugKXcLnm}(B$)`M_ z8ns`aqc8VS4S(|AGaLrkE~Bi>=FgG2DVc{ zw%8X6hHT~RYPos~y5Hy(tSa~xW1K_PSZ?X+y+1)lR-uYEIm(LB-;lq93r&-=wtxGB z@}z9qy#ak{Xct4P!A(k6=Noh3f35rbY+^)5v!)s^jKLbZm&V{TMYD#&*%$b zu%17k_Tt}0X8Bfkr`BD0`{bdI&{2e85lV>TrOTb(Q*=4YSqNwhEYQZh`>!%UdDBIx*QS<+)yt@9yr7yZ*9fhLG0i z%rAQ?dQBUZZR>PWwM^tzipNpR=1?ZVi29DOHU#n8qA)YmuJ1k1l*4JiD?SJy3a;qF zJG|4u-AqYR>Eld2%(4Y!Sz&69{^e-D&vI{BzbCNkdMxr5A=(fiimMKXSC5&_m0my= zH*wuz;PNJaN@Z*|*E#CS*Hs3v(dKa6{*XPBt{|^G!5_;d>%=KF*pP_adAZp*R2U-&u;VE*ULLOqhmX6M zWm$YpQ)t55wDM?P(4~hdZ9z>K&f&;36f(j#?my`Zw`)tEpQ^qh`)*xh`-)`FujVz| z>u)>V+WVSJ+I11yHud&`wX+l5babO71ubc)>FOYLm7EgFn(^>I{G!$ z6-mCg0 z=f}@w;QLnuoa{1U)D!}0=*7GSyv zheBP3;G0unLtnunWE%jw*q_K9Tb!R#m7(OfD*v4B3U$e}i~RfLQ2@*pDA)5VAFJ0x z_SaD||A#i`HTS!fJ+x4GqZgK^yyL$uxWPwcS%D0<;Zw02Pf%8$>bn*Z=+eqo3N4!Y zwoC;8B&Cb3Pk8X-wbf={(vle{Wy57pC{0^xkll#CBW&L^=s5!gYWusN1Vf~H@9sk$ zAQ7nXyG)?AUvBi+yapp+Hl9~NHvrUbevi|AGFP$(KcwKPn7(nY%fvvJ^FXeG@lA6H zi+r+mu84W9#G(5!+c5p^s01Asp6I`+Hh;q(XDrCyl+VENp zo$B5qGOf)&5cVXoRfeC}T6ukmy~27V(paiI7+$YEcsI2>UZ(D-Gl<43rf_>1Ht9cH zAPTw4m$R5>;=vBu3bCqBFmFb4CNH6yON&_%=kePd_l~^{UtDbQ>9^gNi*D$5s=zpg3T#%vwN7OuWq9h69tq zo}8q~k33jj(xlT2d->5RbyFhpI@5q@=6cEhpi_D~<6o^RUjU9om3mu>2|E{W&wVf9 zQP1&58$z+0Sa&4Kr_?Q@Qo{!6`{0OC;FlFgO>7r%~Roh_b6nvkvcRmn%?p8Jm z6;Gxf@PZ?I@6DF*M64x9-;Qzthr~c_=yYU?ChI=TAPQuZoHyO>dT?*eKvk{;?2H3weQm_yxg|5H;dncj(OpQ>~{mZW3e?lMPkq&U6A?Fiwe<>T?x6BvAvT|Wx^_)23 zLPge;h3L6^k-90|%;gfHnI4&KMKQJW{9G-}_SH8{AEa-Bz>}`lS4aM@1FAGemKo#w zr0DoqMv~wgq_0BA0lwG%8%qJN0sqUPwbng2*Ob={lIXgB&b!;_$F;mQhDiJbA~i{w z&D?vS3|=+fIvn?J^fVe4J*aK4iS~zse_KRvEOuQ ze~(Bx>7D)xSwetjG~-Vu+bV8luCoc!QZl(ulgV+eRul+elLOv|kRQG8NPK9auCg_V ziOtjfGyzaE8jd^qgs2pY-N@biXp0fu8h9NhtJf?Qnm_oz4oOXX);BbiMJBfF|0_V_FF zVVk&v-_a64@oIaT8S*LMhE4EuIxUT?{_2J5c|HAt*1>s(8j|h7v;ndpk{_=5Hc=Mm zl=WWBh|vJmzr8JeL?hJJp_f?mhsXX8#^}D4&wmi_*ZFz+Q(w7->V&vLSOO1&`D{<8 z*EY1F2`7q#g8Jy{7WJ z3l!FGqh7S_zkBM9Vfl!JLr)dUan{F(6)o0j25iQ6J_z$N>@%nr-r`ub^Ilvm%JO{3 z;yHR9as671)suuS2qv@lhkbhQ_uOk)4WX1}5WMD50xSHTYD{|nABA=t{LtT638X$d zkBxd&i1@BKwB|U$8Z)+-q7Sa-ItG28E&k@Zi2@CY-SCUfYfG{>OJc8v3N;o*Rw2!S z9nj&^4KUDqZ)#WtmjbLHU>7bQ5kBFZlvJ7dze!PviP+y?H733-uC!66?p~0!AN;FW z4xwaaV-KJGc-3+S?SPuCEzxaI1m8Xkp3jV4_)*VunEiZg)J1TEEr5Q|qG)KSw&2by zh@@}ShzzolCAaeJ)B{UAp#2uBGx^qLcyW_3GQS(z=M=P0)5Q{0vTdR0-nV#1AQ9*R zBh~U?~2Gp{kVb^(Tw_fu+ivIszhDEh)fip*wgQr2H>jL zh>Hz@5+5z5_PpQw2K)Kg)|((S%fn4^cl=5Jen#dvY7}^Tw%P{tYQF=%wG(4RRlvpm zLh-_c4B7Nj9FHN8=mBCyrlu~0&m|g~BNSU(!jc}fJ{cyyeeOxoDf-Uwa^Kl`5RxnA zl6l>}oHRjXo8wYI^iPUx#AXDx;^7%}6Snz#VSV)-SRUwrhQ>B(`?PwZU)j9HIzl{(~P004c;M?Xn_kwzif5BDKwT1t8}sNQ;LBLE|G z@wFAE!h+lUt|k4>>c|G@NgBf?0KXI{0xsIcoH!8l?^@-YFE&_4PbWK^_s=wb?C)~s z1zu|iK7|_m;^h@8|XC zS;%4S&-r?ih$&PYy!Q+E9wPqmbwaN2FY^Np6>pXTh#fXy6cU-)fEuGHbM?|T5V;7a z#L_VPBBz2rEgtaPXhPItwBeiiB^h`7F-h;brq}!UzV;8L`0n%FzB<9I$UUOr^ByAr z@Ex9gOUA8soP2F>Kt|6vXTXE``2fJKN;msVcQA>+BNG39Jk`*=wRS-gm~&CqQTo4W z^#A~4D=Yn{Z4)WHwC^If<}&LxrP|kdF=lez$02 z^Zpa`z6a!jOK`V#xqUh3aJePO9M$}*4OZTWDy5{4(?^>PRoB`D0?&E3Iw1(I;aag^f@I7rK_xr`qZ2I(DMtOD%UPx*h74FwDQ{i1+l!;RoX zzehX^qan(A(QGjL`z4(k01XXpbJVykF61Hc`i6}iqO3SAk4pwSr`mgnBJ#w)d4IPu z)e|LQ9L#Nsp-s=^c^E}XO1$jrahYUJ@XS6pOb1^o$FW_@`tz;whY88coMixM_nCm@ z|INXL4FN_=b0w0=0~eph4_Q8rOE~`WR4A65B$Hn(iWvdwC{Sn7)%L;4RoqeSI}%2glWi^!%#%14TRly^0j`32G-ng!S}fq)i9LRP|;@6Xu@OBityyQz<7 z{qo0eDQc!_8)*wKHZ5vO@D$I-mVp>r+g%NcluoUv zzK2gUUU3rS7SZZWK{?wU?oAT{FIGNX=Td1dDzNNiFfP@cYU_r>9;Xv_rMqwqpA(c6%Pd>8I|spT1>bnxINc8?<)5O zKUQ>#vV5EY6B>MxDt5W;V>IEdnWUc0PFE#iR&a!=iv|F2=l**a04EaMpoQ2mb0U2Vh_QFE%|lAxZD%@ zT#wS0T*bxAR#0>9Y2&zC=|@>ExBH?Ztx9RMB_%IOCzcXfv`u-1o*sg!9vit%44vwi zn~pyJyp_DB&fZUFh~!fEq-JsbAl`hDzDeKN?$svVBVW%aLGD>ZqO-2!V;lP z;iFadb_2(nwjK+#T_GrkQen;Eqr6wf=h5C57(rfNVo?IfHY~`J?k3ul@hfC_k6Cvl z`=t9<8J<~tj>Oos8Qr5vz-3l#UkhoDP(t86Q6N$N3tYZv32rKdaztNG6DDZ(Ql~`7tcZ9KOpry6UPb z0haC}UI+K4p=-qAcC${JZay1NLeRhS4z`i}fR|X_ zhlWv4nq_9O-7IPgMbhEfkETRmKUwxSYsYhQK#-^x>MS2;_Dj~1OxCF)W_IcF@Wi3+ z8&9t5iD9}W{$=11r!a^`IJ7L_?P?di9Q-isNMUGFzbDSm*WULppO4$?@-qt8(wTw!35Dw;_B(*a>Z1u*4#O5`0{_;F=B(lOv7@usFO5HCAJ0D$5_ zurB<|>%x>Z^hC0bwGWyms1JX0{W)u;&Ua_2q&1LTwE3>*OgNK3Tno(()o{%)9ir4= z0uh$O-qRaDtpV}#Txti*td;N)i}1PAR^BU{Zf-Amh`|Sa(<%s#$Q~(+nY7MPuBr?} z_A41E@bR{SZa4cAD5mt?}333Dztpp6a|98 zDOWNDim-Ll2h?KfKX{|d_4X=pb%JJs&USU_9b*W zZFzB2QU1f9Vrb~+1dlo`X?B0~sKvEb;0^6f_mSa1ThFIZ*ZqYnor|mScTxC(XwT{0 zzoTS>iGlZD7^wD0>e?^?7TQRgr%4YcFaW@{8j7)rF#B*y(&b8s@EE;PJsAevr|yx6 zlA@Ada8wsXx^uap(Oz_-Py(&+BX5rTpXXab!`DZ?uIKlv=fx}iu<_|0#9*q?QRiDB zTBqA?@DM^v75G4B1srWM=j(7x0RX|j;D8Xr6_2<&aSsb~YRHfL+0Uj2RpIkgRN$yu zZ=;a<_n!s!=jWpUNdEDg3D2*)aDmT+Gu=)IXLXJgmtR(^(67bNMoDvqKAIN2?*Br0 z{rme-o~ZTItSHOV7vzo$3p>YSIDgx_C0(GH%rnRD38 zoLV62H}$u<8Xm;}fDH;(5m4O6i;UIL=-)P3Y3koBVY;1vUi3!E1i3F^w#QBI?h7zg zjolg}=Ev9TV^uo__4mdO({Cwzt<}uyl%Rb$y6Rc4ibSHOO3X{%8WM^ETGJjI~Lj6D1sG(K>&YEPP5jK{qa|0>~AMcu;~rumSivUpTL9nx-jrjHX|mmuFkHrYO6! zBwkw{;rPeFup;GfZSQPaNM{f!E;nz=RZoIU27PQ>9;TTV^&~XiZZ!qk>-{K1I(;4^ zGT_fAar+V^i~FlMs<=sn@*OR#lRf_wznw%V-c8Ez1Xu(259_o|wIU5>RIPi_W3Vw0 zO0W@RQf(50)H=WRI_d!gaPjbNhOm^J@O5ibuELy`5lvu{%qP9mvFp@N(^6DpK70?Q zE1IQN8@BY%gfq<7(&tMDy@~VVjTqB2@X!pAjEQ&%Q49?XT8C1U%EDGN4hoF z$2%>kE@(4S=sp)G>uNvrau0V1Jif`56#jq*e4(HS<5Q-E+(N#JL#WOX(i8t!`n|Ju z{>XnLxxSd;o}RBK(EFLcZ}`fDI&KgyTf##=m)05(a5szwepzWHOw_+6PwIm%NoHOK z=tq@MJSmfGU}b0{yyz%l)NkNZb)$Mh^9vt!>O8gc1aGg047w{RS1gRSIK@Gsu00F6 z+QbAptiet`@9+$H8@aOIH1Z_?vSculREuFN{Fh45 zPdy;As3xTcudm0-0h%57B`6HG(2D^MB;x8Nz6XZd4Ou&!9=34iYIpD2*g*GLf2n|0 zRYOI6e&`}aw{#NA-(ePmrM(C3nMnk&4uYg!1yF$1o~k)(52QOJSXT_ZCpe~)a;}F; z7%Diu?tq?`GpSNq%~N2D62^S6(UGb(Pxx0&7+#$4MHst6D9eme3r`XxUaQa`9&vKv zZl`r-)3xeMpw7JwHV==$#g~bdCZ=9|sn?eHv}LXAj1AB_ekb?&ZMv){{27yoBL!x$ zxMc8DqJypgPutY$!PsgrG4$s2ZgoFFfYc4@3Uk)-*wpK9L?moGiA(wI_>}v%;HyNh zo({CmmRQ*ms4<GlUgOb6rh-nL_QJ&?g)n{kwM-%D8eyd?Du;r=- zC;l$p;0Upu=xtD5+_>K0myz*Q@WJ4M3 z0&VM(V``1oC#iA$mW~2Q?NG@^y0AyUZZsjBOGW z*EA6XamL1I2X=#;wBxXJ=%f787OXn|Iw{AO@kzhrR54CL!voL`M z6j)&Skqv@~$Q?Lg70SZHVYHYAOW9dm(%U_+tS0D(4VUiYxpLLca@^O1pRmROPB5P;o@Re*l+{4QGFTY*60 zoyUS;mZf2#R9R*8RpwaLSl+n3p43Ung!IAMO~rg7T-vVs8YLY{HkX6BYste-96fh3 z=b4o)kTcO`YJgjj$KfOo|3EVCTifo*aM??~Qun}<;B!ZbgNfOgM3P0jc~a7}8fDr) ziERa+lUT_O>Vt}`+kYyl?%ITq7R%F1HK3p3Ct!@%lPm5@yVpPcha$=KT8&wl#w zBtFFLL!&@9g>Z1F?Z=YCsufykO*@fQNlaQ($^5-8u_U`X`B7wm--P2{>>5zR&Jket zFEq{P8858BWube)a4(>$ckb%9oV5k_f_z((Cas|-dmVE>H?G6Q1{vLK_znseAxc(m zE;W;wMGKpwOX+E1M4Iwaxh4U^S@O^?BZSd37FtN9Z4Pd($KX@x0~z|^Bon+IH_uq` zy#gH8oMkd8u$Wo*EL;xNq&e_(fSpF<@AFR*`D$0&iNmw6|Cxk|34|+I7SO|a+`P`s z{;(1T+hqdNK>Igq&M!)(>ZXt8j9x;|6vqa(IwWeSl7C+&UYWb67Xn^4UuDnQ?&OWw zqYYvOI<~g{vKg#Sf#3tpM7o~$HeS0LVVrEs=dI8IY_U2jnYHB|Mo%hY+9*{xY?#Ym zUZ~Q1Sid6zdu~VGD?RPpT>kkl`8*%}#vYC6hAatXNl4x?^7VK9M!PmF$X>&MUSajkAKnBOagJ<8aEwD(gy~n6Z|H2Tiw6 z$i{WMre7jLR}N)6fi>)aZNeX{inIB+uC7ME$H&zZY%3%aUJc!+o(8kPPIXdXCzv?C(;KdG9Tk14q zHoMa<>D`nZ$aQe*`N|21D8u$`gaagtoh>>wuIsP0-Ws`KG)S}zwV&Q&8VijoB4gAg zV2T)obZ{oByj7RN+}tbvN&9zw&eZRVR6@Fcr{@wYLJkv)ay57_XxaX#b58B~Ri@J< z9XriAttah1YnSyK@9FkK_T{<{vWl`&R{=BZO4R7xvFsK%hnMnD#+65N!9_WAZ9eOOZrtI!Ys`S~L? zs&de9ZQ;V?Z~*(VNbvz_%L3$`dvwOl$nV{M!IaqP{ljN{eQao}*}o#U&B!KuDN|#^ ziqpZjIgrO3a86=V@XZRK9ro+DV0 z0PtUauW#4GKK=YcfsYzwjQeG^|C=Z)0&A4EDC;hOLOZJ8)WX>uNG#~7%*PAI zYq?h)3TEs9Gx#om99QV0DEKd7Uo7D1qXFn)LFmSmE??3#l94e8r1Sx)DGlqa=5~D3 z?&-R%GKKW>GRcjKY7=yFcFCXuQ-!LBuHi=*+doijda!-h7_83;-GuOy&{Hv3QN!~m z#YGOM19Vgu;tHxGm8wk(b#XE@WrCW?2xNYi3=39i7Elm zBRgDHe*sN3L3MOPJ#76<@-#C903*p&5xM6HF6i4<1GMi%llV;XEpk5rBx1@2rRNwm zU+xJ1Z2KHPSNqDccSNwf#ZZ=LVY`F*OkYPV{u?t5D1>h76qdbHqVWo6l8 z-07!&peKW?*r%?POK)n7G{P^&32DhEWL?t9l1)1MjkXg4Hwo^P~qsAm5!$gpQ zOD>z`5a;9a7w(%7zu;}=0vl=Mgr_~OnSgN03Z$Vy%wm83!HN2z7Giovul$*nwj|6o zg2ttj)@m6aNJheEWxPtAP_>MKS=4#|6z81DvLACC^D1_ICo}~RAgPN&n8P7&%P_O2 zSRU-NPc*1o-fl|cakGjv&fuw9VcmKPIF}1R=Su3WliM~*0b}H-u(PfdlZy-ZocnD_ zkoqp7m;y%V)Hw#2E10K6iG{vZ=(%^LFEtUnUa1hCH+Drxeu1`c zSIry2J!!ol{6O=R$&(M;mlAARw zRdRBuS?#PR=i9vo7^D5hh20`OG=QF;nO%lyVU|NH_3EeK^tYxSvgbQ5A9I1>JS*VR zXXzuV8Zkoq8Z&Db-)s7^dspFQXAf#=$=bO6vZ_2^6f3b%sTAA|N#_6mvJCwc%#GW) z6xNfI1{XX4Y4M+8RU!rf|9692AoyP(;C~wA-n-4e{+~l};0C!HD+#6}-o%ev@oU^i zS?!CJm7X4HMB{*kWvJL3De{73zGR!QJWQAI*gQdXHWcT-3hQH((x4kZ$8-oY<@oqX#r-zvoqbz6h#W@TOlb35@{k7!9i=)%eT#x zZP}|+=TGtH-4X1rC&UjGeU9Kz3U{GGKrBVX6Gycu_X|j#b~mHO{pu*&Q+~+VSHJaX zBf|cMFyj3n0dUh45-TDSPcMW3kiPA0ojUumeY=MFl#u76G@cz;Sy>JTus?YwUqY!S z1Z~Jedf^#(KfR7Dt!5#i1GYuwH{P6GQCu%ypC*X}yS#4W47odsNkH2nk}>&{WdI$1 zBK)3hNo(Ns4*)=eX-WQbGHGx4#_jIdc-VjEBA^d@C&Ga}=S={ahJa$^jRM+_!u_iA z#QED3f5cYMj}G1S<|f80%*Xt^PfHEo+mci}`9IIO#Q+x+St!6Uip!0G*7hn-PcKD3 z7J%!V3kqOgFB&w1A$Jr4{*8lT$dj?X{k-*84KMfUi$pi7LQ6E@yzlZRB)p6qz^hU6_QGr$0^@d(aQptenyySNh-%_RYVD&$3|u9t~~8hhwV zWcVjUxGQGQ*Z|@94p|xi#Yns3lJ#*3W#>zz&s}Ri+e>9-gxkrp@e<0C{nN;q8#=qkJc7-=FaS{D${LKzMLd8{xAVN$ zg}iHvw$!wnp`&&5ty+6!4c0=3&UrzKrMz%dHth%P3DLNzmfV4XD@p&Yg3q<6A6U9g z0yK_ceBgD`+n>OEgIE!;RQUf%C4ZH*6ydk-sOI|?`RS6|?c#>KGnf76VdKnA1>9~1 z#icg&tK0MhbGPm`ThztRA45b~uEg3(e`d@WXbNLh!)$HB(wN2M2ZF?c27=Zx$i85Z zVxTt#NoApxijxg2Hd{nOT=C->=R928xFO@*73G7es(YLbv$N~~8F5QIK&6-wCIlcB z=bJ_-ZXERCm)8)lVc2)9+ zie%bqvEDSUjqEW4SK5$+-1Bv=EdJ-2@0v_Y2MFtF-&>xOFcISSH~yPQo16URPp;W3 z-7giNt2^Iyd>sgzsYD4X&kbCs2aU807AI;7lWn^GFwC1Ie>bZoB_^bqu*f7O)}B@T z;tHp#vfoIBg#|rWkVmB^M+#FnX$0rbg^??c6bFYMXGB6F#vU4q$3jBpA;#zP1)bCl zst_P51c1)je!lR8ulIumT$A(xk${_l%wvQk7~-K^M4el3u+dSW%!6*2C|sm3Z-C`F11 z?A>64X*Nf^M1hvrD?Cal_dO<1~Oc@h$&Kpc7M1eN>R)-laXPN%M{>{lLk``NeLSXA+QYK7)QcM+QI6JLtz6a z@}C6A6cj3By429wP;3{5+N_Mkf2b%EexKY+w5>Q=61O|y5=@QI~mvn*dA)Qds6v^clW&gb9HUQGQeK;Y zH}9+BqP>qJ!*^8KM#lRZN-y!dC^e6nj*uIHt^6O-c;5g;oOoy>DH&ySfSedT25wj? z99$Rh^Y5+5tm=Kgu3mueL=szKVT^R^1IvXD74+kIt z!eQ*r@CM;P3z7K2$ppFGOG(~AC`#ptg_2d*l&iJHCB}YI0Dq}6|@cmSfY;?qN&5Ld} zM`*Gm8ZD_OZ%#qo+?w9;XMXD*$UbUaZh8IB&n ziq1nuX2OmE9rup}u<<5GkkG!BsFf#$2~Rx>755d&%fdK{Bnq`F9Kt-;^2qkfp}=b^ zR9GmEkP+qp2}E8flrTKTw@?E9JcC%AfB_u9%o{Z+41DOZaVP}oE8y680}6288?p_} zZwNz!0OqpZgC*{A(H(q?l#XrqrhFSJvomW-r*2!{Xasm!mb$WX8 z{v___;2_F(dg+<$JC*o3XWinXbj`_ZFD`0TN{|e8py>!5g`t2x4mO)<=6Grn1sU^- zN@|Eg%y8gfxG*N2(m;g><`22>(D@m1T69IJ5;&<`5eea7IC_fkxU@9HH5O6Z`)Q5u zBm*dP3lIS!G?TG4LIX(XTV6!n)MScf)U4RV=EA!aFlpk%RJLnG4bID!r)KSytKMO| zfkA$udLnSZW0vrkfX!hvw45kKFQl1aG=7nLQZdxLRX}e(>L#RzO*D}=FDly15<35a zZLD8cJdZ*}p9=H1oeGN?&iyC*WnkFp>aUK?)D~T$(c_`$p2Zoj;X^lNM!gPd`PE?WQR>{#sl$BEVCdY-pUjvH_ z9ZM6748Ew1aU=sAJLv$&^D?XO7cGmOqKE`tiNXZ|;O-(l*2#_P>^w56$hL6$Wz85Foy>=GT1sau%9IHrCA(c>4 zk?qx;Td0F)ju3crVk>t;+w8SS17Y$TDQBcCH0mXwAP4@SBUx5L3^K=}!{mp4M@t=F znZKagtSNjXTAA`HH)PD|ZK#Y{Iu*>OwF1vOO$HuTDt zYg?Lk-PWQDM%|I{;uLHm)`spZHu4&7XLb3rv&HfiC;_C1zCO{s4qLy~pE zDhupy&d$vH3}co#H@ci-(##{7Ok4hllwiYQU!^t*3{pea<}d;Dvc^NhluO|zoEXkb zOyZeO;#vQ!FFJ5C18s+~OnU9rHEo0OB1UfL!K7P0GBH2W8?$ZsALD^Z6 z$O)6U8~lfH$7tKn@ttE&Uq9BYU#(rSytObGTMdw9l(8|-4-2wAPy5ncliHK?h@hWzpX5k<(H zdO336+daW@*_;RwrxoF6Mt)!;m=XTm@{Rr29+xHO<=MHci}Q?Z3HHpgo_qk z2G%czD^vOQ^{geeOi{7Uo;~@mB!Ao#)Gs3Lq#u?-coT>{h$&}=X{Ab{`~0fC@U5Uq zSKBJ{Q+SLj%G1~(jJ{B+;-4-zp{*(eZrP!Vc65F5y6k@-&VZXsA=e3Nscj`yk1>ay z1*A9RK5RG#?VjIYf@tToOfu*e*a!=Q*tKVER%)95 z{HUpj{2uugi`(b`GgovlI*d(iU*q=*VbYWd;a#NjqgVQ({3Rust#)&@O8sHT+{enH zmy&H`uR4Y)ihzoM$bq)LI1`pgMLLPIO-5U~^d2dPP6%aeLqcci)1CKLNp#FZWx`=E z=M;k1jROL3#0c!E^rM-5zAv(Fp!yG@iT*A;0{NRW9ZOS(N`|y?g3J@dIJRVZnv2*O z%Lx>CHj437>bXC6lQ|vJCx_V$t?n@3*!pFiqP2RvAHQDup>MM?u-m5mHR{;#(l`~C zm^O^nPPD{`upCY)xU96Ca*-nU@rRAsM1}2PZe^}*ecBW663z4b&`pI9`p)h`OKX}3 zbo`(U1j$Rz&IjXG;LXkIXU60sgM}^YB3_cBsTq4+Bey1uLqX)68uxB~Ky@gaN^I?J z9xpZ1e&Ji{*hNSfwQg2Q2mWq`eSW&?)<&oEb!J;xGesZ$Gjj^Hqh()5lv1O!Hgp&k zM{U_^8OjeEW_x~1Gh1HphC_s%3SSmh3s#c)P4Mc_!&*I1zm=K>j)J%V?0BdMU9F{8 z7U>{OKF{AWe1=7AlGbmuXPJL5v{4pu&4cpl(`p0$;+7N8jEB;g?;>FxR#0Cd^Agjx zWyY+^`KE@}i4)u3N?~jT;oq&iw}-6NcbAwgRHF5dzcHt9jIWX09q;i;{e|69d7xjc z48ki{m`c}_75@IV_C>$`Y0&4)khm|3lL@sz7Cr%03Y==bon-y`oH)AF>3dStPEgOX zxnhqYtT{zWVzSyuh*oQ56|reFwRSYdl97<%wRM)jaWqP*--!&nby#klJ~8VyaC~A% zFUrgIiZxnmsGdYE>t5dwcy6lx5DqmwTw{(AHnY9H+ukEsV@4gW2ioqpRhlDDt(u{F zuWd$l!21E5Gs-5L|yQjCWY9+?KUon4WvO^*Jz0+|+ zkul;ILhYy-8e>9o^9q_d3Da~INi}Y|(I~dS-j=u03a4zNB*rD#ukvE)9*bUA_O7o{ zBb)by+x_Fi%n_C2dC!{K!|oOt?iR*arwZT0%j}u$@NOnH-*Z$$nru;iy__ZBB=eAV zbhz7P3xOT`jT9W~I-QOpK|i9rm+ zzaLaEt7{#Eb4m8)wa>fPoxA$Xm>rUbmVEUrk#_LyLe{SDe`$W)Tn3q~J#TF9jW_#z z?aUSrtDoMjc>DqFoZ8&0UT?N(kmKQNK1*A=RvGq$tNyvzsC|>;%A?cXL^pMbvb%r@ z75}6GwwovP7TR%MACo{{GIB<|h|QCMl#jg%Hg|F672+arpYSsJkW< z#|qK!QT2G{I>h5t3{7P>354CM!X;0n1ozunB-cez!AfXSK8)bJ>}rws>nHFr>4cF+ zdr`|=7kP9!+8D9oFWY%6xA{bCEiF}%2+P-hI`>1JsIG5=Oe^Dj&;ZM+kNw=QUhC;Z zlUi*ZioyV|Tb9+ioXN@j(PpC}oG7TS$xtCd!q^KWQB zpGdqOR;$i0QH-?kq&6$T6Dxa38v(ST{NakwJsaJKPFOjDP)^8e%dPBGd`%|~&FrG_ z+q_{NU(xQdS26pzRLX2D<2p$H$7yz!Qlwc)jv9A z{?JiXY#SAR=F+sa*>?GXq@{n={q+|Uasf%~|E$IcmcM=dTwOGgJbGaW?*3vsjP+H+ zlqz`3zBe49vPNMPbF^&7p!EqcQ|}Yk=#=Rdkg)$jNmiTneQRq=;Jv>;a3tp2zXIES z_QeZ!snl0LWx?_z@Pu*MRCBhlzJEdm0`^*3Ta(hE6S6hIR+m4^YQOEp%Ha*(svA{n zoo{?BT;S&2<2fw2Hcw_VP^A`yrlU<%3pToJ>8nB->eL_Mxm1?2!1i>LuEXiNSwej* z0{OUFU-8x^iC%qBa9prA{62;Jxpr)Bt4av%zlZCenekS6d)Ks^g*s=wV@gfd14dK# zhKcKgF)Dn7LbTY9|55gS7K0}z2dBMjjjSuSk9ysxg0(uHaZ+tb1KWx=f^P_>b0mj& zaf<$N)Tn+_>x$4oLsvEh0L`n#*^T*QXfrBM@Gff0E-+h+m@3z{xC?%+>9Kx|4LEDm z$jNO}qdKu@GHkB%!i>CgFtPRq*I{bj3-yMMaRbqK^t zdcfZpQ*U$%_+F8z$Rk?p;H-GH>x97>6kYqeo`*^=O{BsyFtTs(_@K?{ z`bnYo`^p8aq}&BzhZ>HZVLbh%0;8`it!bk2>Ec|_^M-yRN5NgbJfL#2;rlX{wpsA^*2!4aDore#7G^D1A!*CT6U@%{ zkzR+VD;#ryhGR(C_z-cV->fUy-XDumnbnfSPiu?vMjJo9kiO^>!$SCJ@;|Ba^Hvx~ zIeWLv5j8bE+~MbLR`nw`Mp>fz?_U%EV%IOa&fdW|G>GBoI#Q}+>Y?%2;1uY*%9C-1 zw4k`t{0GsctzYUJE^YhAt&VG{IBe2m80^8ntinD>gP#rQK z8YvN*mS`Ao6L68axpldW5lpBX7r)`Fy0pEVfWw2Ut|1c;!0_4=zpsetwa&es@wxLD zl_?lw%V*IkcyG&f@0{A;{N2`z7qM}?tf|Tj);X%u%B`WoD!aTyrM8>@{L_u@ zDnX2xk)i)n9g%Nn7NckHq@|tsw_}I@r(0XMZ&Nh^7olhH1uDWA^nR2*#2ZsltQjT0aDiRBp`-#Bj2{yLZm#2Urzi@U)i&-sn zT1v?AV<|c^7*ePqOLb<2Xyaubt*;s|E%(9Iy@hls%nXQpdb6CbHx3SqzOJsmPfjVe zSL{!=<@KNOlxH@Z&__;G#)0C8D!CP>XpZXmy77IN_UmtMcJm)^6>c@hfim&KnV|z# zi(cku3m049dP&V=e4-$1x)|4OmwV30>!1YSlGyD6~g1l;aN%} z(GtaPp&uimzckmnPD8h}C8a|Vlb>X~9ebW>f(r8~yEjjB9S*`zEzB|V#@ZHZ8(Y7( zP5*RlI8b77T~S)XdKq=Iy?(0e^8wEC_{Q^12cT-VbE5iO85~^)H{HPIM!`DI5ezl) zM^?&F+VVz(Ja}nMl%M?CAb530R9#$LV}jORdtPA2zt)gq1H?2X)PF|Zz*M0wj#St& z4?=pIX`Xy;H`KO{hVzY0Ff*~{w-+(buzZ*jsUF zCuBPf-G5)a-yYTVc>~cO{!?~*HZmPiV&G)4t=X-|>N*)vLTpe-4iEQxT%BBym{@2q zYi#}SieB&m+>U}Ea`*i{Zp-t#c#FdUDqvfd28vHa3E+0W?zrgdUGvj?3AtLlFl0>T z305?}J$Q@|tI4q;O$}R&mK9MpuKth9v5EK8 zOAhMM8^2sCPm_jH1qnklRr*VuIhawfob@M*-~pz?WgXxC1W`Xwu;mu^x-cItHlT-` z1lmt>?Bq{=diTi|TCFR4i7=Yvkx!;hjrLnm?Ht=Mr~ z1sh#{y=3E>GYWacQH_uXhTi67`1T#ROmlojQHzL`#HLA;vXHTm)2K-Wm9X`sJJN^X zrq=zzetLawm;Z#z>h@q^fafEd<-DG;SI^(dY=2ytacBHf@efA1ZT)bO`@rTGqAs(N zW#lRCux`Hdl!=-1%l$ENOP`O;3~T~;hmJ1Dt>$g$vwH)KE}7HM-djN*i%YN<9sCvP zTsMl~wVbS})3U7fvh+@m#Y5(YiAG*mTsh#0g$2A0Sc)GlB8PaDv@hbE zx^uepk3U9`HJ7nZ-bc=P+qzq1;%Zt;^f&gJt7qYS6g8dB=b=(c$3GFHuXcUrO_CH&pFN@d>s<~D}Mg>w8zaBNxKGm@3$42*K#4?`ns)o#?C#B zW71Z^g;rT15SLMN?==nIWJ3pQe#Buxb?E*_73@%1T)m6ReVwEA(GaL%Zp`=F_m^)Y zdy8=OzPt z`cEuILAM9IQ9cte!1BC0?g~NG=(2{B!JVak0k#Ubo+y!yYqgcmof`XQL&!Pyq+0$$|mIAT=Zrnng!26-pX2kcClR zWS%-=CbBaGtFr|hkL+8E$9(r@VMQoQFhH)^fj_ z&EvZW);j!p=QeiII+KszXF!YbfUH}8EZ6jggo2Mt)qwL4{g*%%423e_emsCzQn2^U zpT=u%gzbdZKLlJYqJ|_qd@>0(i;s=rv(uw$iWiPu;@V2Ko-trg;_4o+Cso<&9@*&g z65(1)44oXH{ImV~f4BgRY-6C3?)8rlvE#4a%+g?hRM7S=BfumEXCvVP93uY){)OYB z16DAAfJKN{ZwN_QApbF;PH=uZqMnQ_5Y-xk!;K;#A?4S5Af#QV*U9os>Ot3U+*Gg}LjAxY@Ei4QT z4Sj|dGLL+q)!%ExpMm@@x2r@`Rfp+b&OSsUuqW`M<=OYdq%%8tlA4$Lp(SRqb^M_7 z*RT4He724Hv{T-_KVaA7u(CV|E9~aix&ZOeoie-%fnn#FgluhGhq~=$rOm2J)DMnA z&JPm}j+zV-LQ4n*jsxl7pp&0o67KGu?HWu7-q^s7N?n_x|Tf7hXlo zL&;oiQHE(+y?EBjtl7Gw`{I%s@{eB62Lf@xNFEwEdY;! zy1FPI8=4v#iz{u(K29Q&6NdHmudp%wxB;TTPO+;lts6ASgouEG(r-8B%!((^>Svq! zUT=J?sF0$D#jm0%nw5y!`-QA+c8iRQkJwO*PBtIBE4OgSj%@L0D|&hgu->a(DRSO-r1SF4Ca)gMEGC%}xV`ovRJ zr_?kLps$ocrI`jm#H4B-M&m7N1!?em{VEl|&2FO+-pJb!rou z4@m*8AM9|ZS@#5T2ZFl0li|RIO^GhC?UE_uRQ$G>Fkl0TT|C8vm%B2d(9%s%B|9jqxs@wwb$gq zBYA{CD1|f;u~=eC=WfEd%EXyV318?*pj> zr_q54_gS?B!18 z&TR%Zj$*_Z&R=&&VAo+TTmG>63z-DkEcoD64lIxzJ(hvJFhHWH$23U(x!!J*om$fm z{F1Ylm8)Yjf&L3pJITVao9X?`Su@Clqe(0^#U&RxeQsX%pHYbdZ3A{Kv!E$77VUds zFyY0GO)dNMK7#5@^G*v2o+7d*fEogx0S!(l(xCTW3mpb`--=UkPhTORhRY3?aqrME zgA0dG);t6GGCQp7X!-tCbnPnHeUp20oyyfaAuX4+!z4pw6J~}83qJgIc3uz*q z3mwaj!t<&ti7d;LJ^F8VZ%<~hHGccfz{Y|tkw8M$HZEYxsyt5h@SOq*vo{AW7E{Q8 zB$C8p;7=}fjQ!!dP?HkeGnjpJ-OEk$slG!yH;e0RrC?=Pm-#J)J0CG6qiO_9&f}f_ z8P%XBD{GyvucJl$u1qH5dLtXZpKDa%Lw03iU4Qb91%*K-Mgo8alDr-xKzy54fL>}g zw|7GI=k~f#LVzF+0*@WX?d8^V9fueTba{fTcln(MKgM>&f84C9hH$Qt@~nLwrGkb0 zY<%|ii!8{{cl|>Wyh`Q`028$YQBCJtj=##Q-o2Vaz(a-lLG=JQXv0_n%)UXwIy9*F zT;Rihq_ns@D=WddfeQL=Kl|6T-}HjZJAQivX510l0xC}8+1lvk;*y4bPiYZZREDgU z6fV^7`KG$o>+9WbCx9)%V7zdmEMN7G&8gq@&`mIUFHR>V6-Nu@q+-CEnXeS7V|h*o za672p-W}Y6pX$G((2ooCgDYjZ=1(yLZ0dt$JJ{2HiFV&h8D5X%@mPi!Yn=?|JHyFu zcQ1HveUc_Z%p*3Ri1N1F#|pX2x?$@!}E}=iXL8C zUkk1*0>o7|CScQ(%JrU?=Ch_8=2y;`-joA`yC!Dvq)s!nE^#GZ>v|M;6NU@?8wyS4j}1|ts9`D=3_so~O}g!!=mFS-rLz7^*)Di4 z*PB|c8Xe7}mdkC|YVE4?$VtL$?GyNY`aXK3c{7?foCWq+A5pRI8YhC1a%N`_nSzEG zLQqim%Xoe+t2q}Bzmd?JwgHR2Hhf4~!C-P6k&=9mCl$2c)8a@2C?mqRdTnI-6h(XC zyViqk5v*2u9Nk9igCkwGxp=-fssK2I1O|l86F+^zv!rEI+kkR~htLQDZWVw4KUhEb z$XrBw6oZydGG1(7NhRa$|9LP<*G3xXV%H9FVS;t7IXOh@OmU_WTWzw^D=W?8e=P_! zaMk#P6xyy_asl;72&n-8D6IPALa@;yfS#{%1}(@Zr+!Zgrx0#-1h%(qn?2>&{+w;# z`mE^uq2j`Y)z-O~T4l+s$*vOjbUkuZ_-1}|u5S<9{4wU{QJr95V8@KM3pQM z6ZDb?n+>)g35%NGjzb2x4V|9|COXC-MSz1tSk6QS{uWm| z)ZMe#ZosoDPT~1D?>k*dn)+gBJ`*Sp&d*|T%_ptO7i<K3(Y4j*V*M&T{@ zAxEjo4(9N6yO_N+hn2SfuK-9DY)1`;T!Iz95d6^c(c*OD~j@O(n* z2hjV(U}O@MEE;i74P<<^a6Zwwlq>#Muf)CP3QmJWWvl*WY48euGN}FKcmV_O%StZY zc?iI8cJjj|h&qTYfCNYo+$nY)&=gr-OQnul)i<>GN2kLBy{ackJm$kc1=u5hBSOd73dq@1dYrjd(FVN~}vb+=$+b0Bm*y-z6bo=8u@=6eJKRS0g|x(|-f z<5}4Xgistw9@6l#2l_XRBVoL>&}Ka#?8{Jsxxw8jDC-jc`b$A3z_-rx*CpHga_pP= zYq+;{Te+@v$6wk%Qf1de^lnhzz6=a#Xe7?c=UKBde zgupVzX1H52M9aY;m6;@RN9g|5WzHMY2LT+&soIa4x<0%rsF%zd=A9Q$`|xzzbRHt> zA*~<&X>>ZgmM$>i6lZJP_kd#u5mu;_Ce@og+)jG+9;pl(M7Rh6sTll#;&57t$cSvU zgoe+nV0^XitGZ>3pKC0zhnII{*R_0kx6!l^=2yj7m$O7UI1e2*)0F3BtC5HSC)#J& zYT2%>-exn9|9R*<3E5HRdB5u1J9UGSI2;f?Pb9oI1V=MMa2}Wzy7n1Yzz^I6Zw1(0 zWm$h%o+5|AB3Zh?ufdUGY&! zJu#d?ARM|M8MzoR96~!cJ0pseLjy};QXT)o^4KvD8q zAb?3tk&;k7<}JXq4}_+-k^^qhNZQT=h6S(J8lLEIF=YFA`; zexW+iWmVO`Zf7IX;&D@^)8(Z}@&Nf4j7yc5>>s^bMLUmw)g`}FCjUvkjeTk}-I1AN zqr^{*Oo?3F+gaVVi&T5-4hyxE_~yQ~F!vSZ*Q5dH74y*!{55o6Fg^8zpL(&xENGFF z`Ih}Y=mS##S69XTITdh*_wVg14>@dSBhZ76`(6$4II`|Oe5A}i{6EbYCY#xH7dls6KM79JC215$YTlB1@`7ty=v4{igdfl39H9@{p1-$cbV-j z0pS|OIAdxm$c7+WX(O+RXOaIW)90wqzVJzh_H)FO2F5>A9d&0>_O0e2-+S5p0S!FZ z?d>6?GU@tMmKV6WOoSrd1>R5YMW~H~eCBCwI)f80?4cu1W`QlpHR*b|5zk?)$NaHk zNzl!NdNZgp2TJ4%&XTFA*9=%K2`_gvw~D)&By*!sMThReGlE6!Ii{> zSZ|dnkA`ohrqS2s@2jr3cs)nX{=K}(CbU=iyZa1=SFRX9`6UzLHq$EG)H-69FKb;$- zzzhoYBH-}iwUIRkUwkce;QGhP*`OA4_p{06bR-G%7rC0jV|%3Ev`)A8ETeh*0lR7E z@X2-O^~r@)pkVN`GZvM;f}br&nV7J*G=f~i+YGM4a;9H~2ux=ErMvdB#H;3QJkb5!jD)Cov^g-`;tAVWZ9wU?jB2XgeYiCWt- zx$-M(mA~0M{4+C?=Iy*42ySFr9`~{UPhcXiH%{~GJ;8Q}YA;VjJ>Q+La!p(A2p_+T z>HAOTAh|wnzHxyN>1rP1(9w$ZRY!OuSkyT6Heaz}+Y_!{-0K%O9ISbG*YE0E^sk8I zj_P0DquMio%7WlS0QT~8#ArR5jVz3b+2+5T$}(8iSm}N+F|8|T>#<5OWqUY05Fc%v z3u;`qB*3a50hhYlk(oYEklkLGHvKXLmv!%BiL6#>xb8++be=P;?LXPd6{73KQEA+T z2GLygKYAZPwdsr1i_Y~6<7F3R@f)uO9$c9Va~__>9v77CgHB7n&aP-o0*9m|4}hMG z<`>gHcyip6m1dK=LxkF?RWogNk$6bzu-T*SE7kNK<0FBlP3rxqpU=*2H}X~ezllOFM&tohSgn9+jv4C${>|Nw+hCS~d~Ao6|^OP%2FM(NMR~MF=Oz z6lq4%+6$(V$_c_>g#SuxVGVtb?Uw$$Po4O#sAss-@gpnq^VOVlo_UE9Khh?yo$PFN z*0jjHg3u~oVlcOUPy)>R4}K#9sG6T1KEHVV+NdE+^l)ZoZ*$VT)RDBlY+Xl68Wp%FGrbHbb~EpdvBhyx}nNfzb&PCR%o5COmNymB{X&r{XO@^{@PwsPlWT zqg<{1TPx1}FV7vce{2ZK#t++=-4~tH@Tjf-fS2*Wi6%%-(X6}SSeS~q0<570&=b~k zHz-3eMDSzD7n#$yCZ*_!7~}2(Pwz8=bDQZi2HWsV@@-oN@}toRJkbS&LQC#gc?H3{ zt?EYiQW$v(Xz();sOL1h4Uq?R;mz-udhWOQ_7A5;d_P@Lya*tBW*$EGkaZZFTEJUt ze_B}x3kk7{j*)JaG#8qOkdu^UKMFUgVD~NsB?g8MLw0{ZjW%!k3BF@9^<05meIW$j zU%#hbEJP3~-8zN~8@wGrw!AK=Z~v#j0XSwJW#HNHjr!JJNGea2ec;LGc?dOu7hD`V zBm|5K4uGXhGxojm0}vBDe4%PzcL_Q-$g~KXH~0ux6t(`5cHmxoo;rRXIU{OnS*rZi zTz!(>J34eWV3(Zn6>bZ(x3u&0xn|eL`PqEc`#1(`sS849TPxow1jEqQtDt_+3216F zV@T3L_h@5^`-rjeKHwKUq`{y1c(#2cnpp~Q=>qC#j)i(&M8t4)UE8r0;sm!EbK^z}t868nFG5ah%>Frzl=xRHoEm=V>Tt~bVO=JCl_ZK1q;6rkpqFZA?YW6>g1;L##LSI#;VOs)}Wz{J;v6@ zGNadvAb$|{6>sFJiLv_R9lfuP8Zps-gSwejrb0^rb~6fRh=>sk^cIb3IFtOSFXN*4 z7i$)qsGOooXpX7i!d%goMYPvsr;n=)sU8kN0viGnU^}q0{YtwqS2?EpA_PC!$~;-V z>;#odgKx|evGH=o(}rj#nE|>{Crcp5S_j<}-y8_O5}^n<6IfR21!lizdx*0+m;l?m zT8TZ01j1V|b2=!uY^1Yb`sIYt(xOLZN{{Bo3-@(o>}jO=i49)&3+P}oaS77~=0%n1 zjI6-tD8biQHYAi37l-iPWtJ(8TCYLk2l0eE8Zw3l2C%l7L|!(;o#;1Ky}6j(vh;Eo zSrr7l34F45y@#M0fSzE_R^E>uUN)iJhGOJ*((DL(x+G&3QrIpZ*kD)T3m0h7e#ptI z$C%ld8dGRWScYri1A^#RVsIwyU(^^;3H19wY|WPb~U_LHChwWo6# zEGv0zb9mo4D?F;3tbWvsd*b!qYha=JoZ@=zpb`&!4o8#z*wEbIbN{lPmQ>qIgXCpQ z{K)Tt1SZa4s=d$#UWlNjKPZ2ho1&6tm1dMxK2ma_lvYW0h+<7IF@4E;4^PEgzJxI_ z3)_NdbD3zYPy_c&V0j@y=o1lW@}H2by`0t>9WASx?7C%WjXr*u_ev(6UY#qbTveGY5@Xb~~m$cxjkgoJhiYC|az zM(G5r`0zMP6(+3pPGAvCQ!9jhgZB}Cu_y=vn0a+v(@Gtg_jmF{$Td|cDQY3}H_PgB zXWLz;*gm$D)$d0E56tX0)UClePz$Id8nhn0kpY+*WNq+8it+u;StL=e(OEgSgl_R$ zOnL??opQJcMd1&{M6AN$Y$C;v_>)Mwzotdx_}N_Ri=q%0?+itm4_H|(f0<5 zbcR(n!J^@^=S9ixm};MAQY1ng-g5Hd`cHm*J0Ra(F^8&tmfMu5jZO{};%~+;lUk9;sBC5e%8a)7erv^Nj^`&WCBtrmN24i#&j>HQ-be6xdN^o(Tol zaCuxqSxsgOZc15}*H~mzaYFBmU3GHI_zw4;*qyu|{D!(a*X_*&HpTh+LPsb3H9hMd zRng6UCK!{9@KQR<#!wL(NCq%r;%I(G|KtasilEucM(R#A(z|A^9Q8r6`_Su|K zWOiy_3iwjzu8$P?EY*Rdj;d9#2{7#MUlQC-&)%kIu=5>lb5|VrQbIY}mP}8!=lAd7 z{V+zUrCC#CBrGL(zD3YytEZi4URA>`7<1l;XCwd zZRA2KtEsiUE>(MkAWtNL9iSD8;Ci3)al0OT&O37D3v z1l@QQBaFIdvJJT;1x`y)+U1uRRM1%F^F$Q(9Y$yM#bBUwYBwD1a{V^I^aZtV4fXqi z2;!}H%u_CZ_!Vm+b+bnocXgpE(g;lzMVpP?=uiw`TI2_m)}CyjXznVN`NtJ#GGH-S zek(T_z)InKIq!`^H8NNd(nf-ANPyrfUIa-bmA2$tIQ&Li^+GG3{wiY4)1a0~Ij{R+ z?fDD)n}FUD8Q;qT|MU2j7k+=p(A3J+4?pwPI2Gj%`z=y}HU0o+!D&ds=d{5}zuGpz z%5Shm#pP6CvEZ9F6_Y8HrAMRdtiBc$FCw&g6sRy);<0S(}Bt|;VLtdDE!`E_!bMGHmjENpJ01YSN#{Nuo7JX&$YUvIP&K; zeb84FL0+isB%Xc87_K)$u+1m(I5Kxnz!-n8foxrU1`z?8q&L^#QqwZ@sHodAWgMx~ z0`3N}QBX)_mS^d4xV_8S`k9Soab8TrXZ?KVRlQVS`Khd-ijc_CN}>S$C74j_>?fAY z(dk3EkCx@a0yxsPN|(`Gz6fp&FMZxUF=`YoWdP*q9Kd0|<9b6>!`m5qS83XYJ1eDE z!+YgkQhQJ2e$@=|aeQK=fA4wU?wvfV^{7YFlc`X&>-t&w%I(XxC1nB? z9nSNR0Hz0-lN~*WcCwP ztC8S_c4=|3HjiqNje7IQQJ(_%E)+$L8i}Mx)l5Z!whB+@7jLosqa%9dM}8kn-#7d% zp3GxTb!@N8WUzS`No5JMK$oPEs6UK9qRM;DyE(_u6=#Lzz|Dt8b17uM{3%~3vs4DgZB=rM zyYDW>2{^Q?%)b)YgQ#HtKc>Dis;Vw(_nbq6beEJM-QC^Y-Q6h-2c=VyZb3jABm_z6 zPU&ut2I)9=`+oPkd{SrZ+;jXv=oyI51qdzohlBo`7nt{&Nzy{l@tEc zzLI6JD+R6isVVWa8?y)jCVryo^F4Cn`Iqm-`F>uNZ~4ARYX0M&-KL;7T{DL9=&Ba; zd^c9JdpNP}9YlwoAEFyjRQLsjZy-IhoSTMOlv8g|bm(ScJq_$Qr>5~CMzbKSjmcof z3pO1_eXYs9ME2+&_BoA=8oej2T*b2LmurzTD+{sx&`gh@b>x?a^v*CiHS!(-lTzR` z@FlA>^J%g0UOhD3^~tA`E`3{D@;>P^O4)_D@V`$+~=PNWyq(!q_2uY^;Q;!7Mg>i8aLJ^ps^42fr{oi)F!!6)8_JQtjn| zk3*2(f6f#@853r@z_I@Ua>;;`erOM4XYcvtdu8*Va&rB7o|zTJ?mUX>GhdyCzkN;C z`EE7vojKLwcB3Du&S}0njRehE2F^WT$xLm9hWx0h{M%?hRzyUiu``^}q;9%$!HDe9 znvPTJnv!zJ+8C$h{A%DuCg$3QVQZH(U%;PLDr1Ooh5uTmS@8A}lWq(N&~2qu6x=xI z1$+sIr?yH?MKSSgd5qGEHVaF$qo5|a$GwREyyFpk!&h)YW@+f(%Fks2EUGMe^ zDRG`lc#>x+nLNSJKOW}8P)E;!VJD~2)al*i&C;He;A7eB%C@l8OCuOvcb+8 zYLaiJZcl6>H6zB${SvNZ!ZTu=kl|Pz+3S(4sxl=o1Mxi}aj()O`!@SZJa}n6vFbN2 z=5YUus!i)pm1=t_qXv&}AKu4!582NW<99cjv=`@t8dvO`mA?PpWP;sG1nqCD<*o7g zMj5aFv*om$0>;0%03YTve|uLbF#>OinyRt*`|fYYs7+&K+N@?eow1jKtbyU!`@b7g zFNTVc(n7HN^K0O}d!sxr*m} zoj2&h55@cAi(uXg{U*4 zds⪻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

r@R8DskCg$QY%d=x6zD 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

!LxQh!NmF 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.UsePlatformDetect().LogToTrace().UseReactiveUI(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json new file mode 100644 index 000000000..7cf886e7b --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -0,0 +1,1720 @@ +{ + "version": 1, + "dependencies": { + "net5.0-windows7.0": { + "Avalonia": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "dependencies": { + "Avalonia.Remote.Protocol": "0.10.10", + "JetBrains.Annotations": "10.3.0", + "System.ComponentModel.Annotations": "4.5.0", + "System.Memory": "4.5.3", + "System.Reactive": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.6.0", + "System.ValueTuple": "4.5.0" + } + }, + "Avalonia.Desktop": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Native": "0.10.10", + "Avalonia.Skia": "0.10.10", + "Avalonia.Win32": "0.10.10", + "Avalonia.X11": "0.10.10" + } + }, + "Avalonia.Diagnostics": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Controls.DataGrid": "0.10.10", + "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.ReactiveUI": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "dependencies": { + "Avalonia": "0.10.10", + "ReactiveUI": "13.2.10", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.0.2020091801", + "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + }, + "Avalonia.Controls.DataGrid": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.10", + "JetBrains.Annotations": "10.3.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Controls.PanAndZoom": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "zIQhp86CdV7xmFXFkaQBDNDr0WSyumEdJvqvIrywG5SEQK3HzACt0gR85KX19DHTlkJlnUVjmfkTEiPjwvgGtA==", + "dependencies": { + "Avalonia": "0.10.8" + } + }, + "Avalonia.FreeDesktop": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "dependencies": { + "Avalonia": "0.10.10", + "Tmds.DBus": "0.9.0" + } + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "dependencies": { + "Avalonia": "0.10.10" + } + }, + "Avalonia.Remote.Protocol": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + }, + "Avalonia.Skia": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "dependencies": { + "Avalonia": "0.10.10", + "HarfBuzzSharp": "2.6.1.7", + "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", + "SkiaSharp": "2.80.2", + "SkiaSharp.NativeAssets.Linux": "2.80.2" + } + }, + "Avalonia.Svg.Skia": { + "type": "Transitive", + "resolved": "0.10.8.3", + "contentHash": "w7RYf+8+gOI3uVZZJ59S0EP49LVsyr1jpnZQzVFQqKa3y/c/i2jT/EUoKOeaqPMhFIsQZyEF4iluqoo6aZ05Tw==", + "dependencies": { + "Avalonia": "0.10.8", + "Avalonia.Skia": "0.10.8", + "SkiaSharp": "2.80.2", + "Svg.Skia": "0.5.8.3" + } + }, + "Avalonia.Win32": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", + "System.Drawing.Common": "4.5.0", + "System.Numerics.Vectors": "4.5.0" + } + }, + "Avalonia.X11": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.FreeDesktop": "0.10.10", + "Avalonia.Skia": "0.10.10" + } + }, + "Avalonia.Xaml.Behaviors": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "rHDkieWZDTjG+PVGQzronzknmH24r2VDtzbNfC3O8FLZGqREsBoCRDrqW4R4bmtD6CqpDPBey5soBYnnDE1m3Q==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Xaml.Interactions": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10" + } + }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "bOJvciyk6kUjPx+mg6n+bwHQqRqgNiTDzTBkpokfkcWl9pMAlKvqqUe6YXWVCpKIDBjbzvkAbYa29S0ajqwFxw==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.10", + "contentHash": "xxWrpi0HsySczpU3Zl6c2ugbkTOs9qwqbvClfi/AKncoVbWpXv7W6J3kfQcfRlnKFwkTPjLyTYKVERIkb7kNCQ==", + "dependencies": { + "Avalonia": "0.10.10" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "DynamicData": { + "type": "Transitive", + "resolved": "7.1.1", + "contentHash": "Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "Fizzler": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "CPxuWF8EPvM0rwAtMTR5G+7EuLoGNXsEfqyx06upN9JyVALZ73KgbGn3SLFwGosifiUAXrvNHtXlUwGGytdECg==" + }, + "FluentAvaloniaUI": { + "type": "Transitive", + "resolved": "1.1.5", + "contentHash": "1W1VZQaCeH4/kzNM2c9yPHAVVs9lW9/09bzz1lqu7Tvu79u9JCOjwkZmR8rGC0KbyOA7twwVr2/VvB84zDZYvA==", + "dependencies": { + "Avalonia": "0.10.9", + "Avalonia.Desktop": "0.10.9", + "Avalonia.Diagnostics": "0.10.9" + } + }, + "Flurl": { + "type": "Transitive", + "resolved": "3.0.2", + "contentHash": "1/6mqdzGCTdAekbWkVZBTylCV+8g3JUSTXRBngRVR274S+RsAYNRF79GbDoDsPfMKu8VPc9HkQWdBEAncK1PQQ==" + }, + "Flurl.Http": { + "type": "Transitive", + "resolved": "3.2.0", + "contentHash": "5S8YiJm5CyRFO418GG9PDrsgmGEaZJtZLUqk3xqBKrfx7W9eKtOSpeji/GwAL+tSgNTALKBPvBKTaT4S83Oo+w==", + "dependencies": { + "Flurl": "3.0.2", + "Newtonsoft.Json": "12.0.2", + "System.Text.Encoding.CodePages": "4.5.1" + } + }, + "HarfBuzzSharp": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.6.1.7", + "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7" + } + }, + "HidSharp": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.11.10", + "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "0GLU9lwGVXjUNlr9ZIdAgjqLI2Zm/XFGJFaqJ1T1sU+kwfeMLhm68+rblUrNUP9psRl4i8yM7Ghb4ia4oI2E5g==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "LiteDB": { + "type": "Transitive", + "resolved": "5.0.10", + "contentHash": "x70WuqMDuP75dajqSLvO+AnI/BbwS6da+ukTO7rueV7VoXoQ5CRA9FV4r7cOS4OUr2NS1Up7LDIutjCxQycRvg==" + }, + "Live.Avalonia": { + "type": "Transitive", + "resolved": "1.3.1", + "contentHash": "FIzh7k2PWsgIBjS4no51ZzWxYmzTG/RzC0DUO6PzoiKkqyKpvSpOvcRg+41Roz6X8VnYCIVm61R7TFlT4eUFKA==", + "dependencies": { + "Avalonia": "0.10.0" + } + }, + "Material.Icons": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "4UIT91QbedjNfUYU+R3T60U+InozFtIsP1iUzGbkq/G0f1eDE3tXMWUuLEDO3yCEP2MHrPjAOpokwqk1rnWNGA==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + }, + "Material.Icons.Avalonia": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "hK0MQm2XyPwjT+DiviDOBjrJVQe6V0u+XTDVbxohkq58hUBlq0XZXmHHZ27jUJU6ZVP9ybu44aXfWycbVjnY2A==", + "dependencies": { + "Avalonia": "0.10.0", + "Material.Icons": "1.0.2" + } + }, + "McMaster.NETCore.Plugins": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + } + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "2.9.6", + "contentHash": "Kmms3TxGQMNb95Cu/3K+0bIcMnV4qf/phZBLAB0HUi65rBPxP4JO3aM2LoAcb+DFS600RQJMZ7ZLyYDTbLwJOQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "3ncA7cV+iXGA1VYwe2UEZXcvWyZSlbexWjM9AvocP7sik5UD93qt9Hq0fMRGk0jFRmvmE4T2g+bGfXiBVZEhLw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "2.9.6", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Metadata": "1.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "/LsTtgcMN6Tu1oo7/WYbRAHL4/ubXC/miEakwTpcZKJKtFo7D0AK95Hw0dbGxul6C8WJu60v6NP2435TDYZM+Q==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Scripting": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "tLgqc76qXHmONUhWhxo7z3TcL/LmGFWIUJm1exbQmVJohuQvJnejUMxmVkdxDfMuMZU1fIyJXPZ6Fkp4FEneAg==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.CSharp": "[3.4.0]", + "Microsoft.CodeAnalysis.Common": "[3.4.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "+b6I3DZL2zvck+B/E/aiOveakj5U2G2BcYODQxcGh2IDbatNU3XXxGT1HumkWB5uIZI2Leu0opBgBpjScmjGMA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Bh6blKG8VAKvXiLe2L+sEsn62nc1Ij34MrNxepD2OCrS5cpCwQa9MeLyhVQPQ/R4Wlzwuy6wMK8hLb11QPDRsQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Ninject": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", + "dependencies": { + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0" + } + }, + "Ninject.Extensions.ChildKernel": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", + "dependencies": { + "Ninject": "3.3.4" + } + }, + "Ninject.Extensions.Conventions": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", + "dependencies": { + "Ninject.Extensions.Factory": "3.3.2" + } + }, + "Ninject.Extensions.Factory": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", + "dependencies": { + "Castle.Core": "4.2.0", + "Ninject": "3.3.3" + } + }, + "ReactiveUI": { + "type": "Transitive", + "resolved": "13.2.10", + "contentHash": "fOCbEZ+RsO2Jhv6vB8VX+ZEvczYJaC95atcSG7oXohJeL/sEwbbqvv9k+tbj2l4bRSj2j5CQvhwA3HNLaxlCAg==", + "dependencies": { + "DynamicData": "7.1.1", + "Splat": "10.0.1", + "System.Reactive": "5.0.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "ShimSkiaSharp": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "BWwwsIlYUFF0DUc8Pa9xONIXVDvEL9pOYc9YmWilpHrWC37dcK+H4+tfuxztZxtfJx559HGn+6iZmMDjfFoOxA==" + }, + "SkiaSharp": { + "type": "Transitive", + "resolved": "2.80.3", + "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SkiaSharp.HarfBuzz": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "dependencies": { + "HarfBuzzSharp": "2.6.1.7", + "SkiaSharp": "2.80.2" + } + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.80.2", + "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "dependencies": { + "SkiaSharp": "2.80.2" + } + }, + "Splat": { + "type": "Transitive", + "resolved": "13.1.30", + "contentHash": "yaj3r8CvHQwtvhfTi+dp5LpIb3c4svqe/tL6LdAS8wWP+dXAp3fTCLjYx21TrW1QBFTBJcg9lrJqDPbheSzHbA==" + }, + "Splat.Ninject": { + "type": "Transitive", + "resolved": "13.1.30", + "contentHash": "hYgyD12Syt2l8U/KccMzNUj4nmrdULjoRTF4g5Q9XtVWPrcdTYmLEdcX/prZEWaFT7vGNP6x9uFXvOlM7Jc+gg==", + "dependencies": { + "Ninject": "3.3.4", + "Splat": "13.1.30" + } + }, + "Svg.Custom": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "6FnbI4T3uCNN7DYJpfPFa4caTTJzp4YbhU3J4c/syX7wQNSeQ/1u7JZZ+dGgrRUauiWP8VsiCLKP8qinc5xI5w==", + "dependencies": { + "Fizzler": "1.2.0", + "System.Drawing.Common": "5.0.0", + "System.Memory": "4.5.3", + "System.ObjectModel": "4.3.0", + "System.ValueTuple": "4.5.0" + } + }, + "Svg.Model": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "F/rimPwV5KF64P8oofXGMwOZ0T7b3z1A9OiC4mv5OdSpLpMpUxpSwGLAOkJ5DFqQgXqVjKKLhPdjIjQBwy0AjA==", + "dependencies": { + "ShimSkiaSharp": "0.5.8.3", + "Svg.Custom": "0.5.8.3" + } + }, + "Svg.Skia": { + "type": "Transitive", + "resolved": "0.5.8.3", + "contentHash": "ajQ0aINQtEzWkqEXyJjnwqOFNusWNMHJVGrKa1ISbP21nrWJh+tApydLFVFGGjs91d7K3YOUbWDKlEzzdDQaOg==", + "dependencies": { + "SkiaSharp": "2.80.2", + "SkiaSharp.HarfBuzz": "2.80.2", + "Svg.Custom": "0.5.8.3", + "Svg.Model": "0.5.8.3" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SztFwAnpfKC8+sEKXAFxCBWhKQaEd97EiOL7oZJZP56zbqnLpmxACWA8aGseaUExciuEAUuR9dY8f7HkTRAdnw==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "5.0.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "HxozeSlipUK7dAroTYwIcGwKDeOVpQnJlpVaOkBz7CM4TsE5b/tKlQBZecTjh6FzcSbxndYaxxpsBMz+wMJeyw==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.1.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "+MvhNtcvIbqmhANyKu91jQnvIRVSTiaOiFNfKWwXGHG48YAb4I/TyH8spsySiPYla7gKal5ZnF3teJqZAximyQ==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "Tmds.DBus": { + "type": "Transitive", + "resolved": "0.9.0", + "contentHash": "KcTWL9aKuob9Qo2sOTTKFePs1rKGTwZrcBvMFuGVIVR5RojX3oIFj5UBLYfSGjYgrcImC7LjQI3DdCFwUnhNXw==", + "dependencies": { + "System.Reflection.Emit": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, + "artemis.core": { + "type": "Project", + "dependencies": { + "Artemis.Storage": "1.0.0", + "EmbedIO": "3.4.3", + "HidSharp": "2.1.0", + "Humanizer.Core": "2.11.10", + "LiteDB": "5.0.10", + "McMaster.NETCore.Plugins": "1.4.0", + "Newtonsoft.Json": "13.0.1", + "Ninject": "3.3.4", + "Ninject.Extensions.ChildKernel": "3.3.0", + "Ninject.Extensions.Conventions": "3.3.0", + "Serilog": "2.10.0", + "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.0.0", + "SkiaSharp": "2.80.3", + "System.Buffers": "4.5.1", + "System.IO.FileSystem.AccessControl": "5.0.0", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "5.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "artemis.storage": { + "type": "Project", + "dependencies": { + "LiteDB": "5.0.10", + "Serilog": "2.10.0" + } + }, + "artemis.ui": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Artemis.UI.Shared": "1.0.0", + "Avalonia": "0.10.10", + "Avalonia.Controls.PanAndZoom": "4.2.0", + "Avalonia.Desktop": "0.10.10", + "Avalonia.Diagnostics": "0.10.10", + "Avalonia.ReactiveUI": "0.10.10", + "Avalonia.Svg.Skia": "0.10.8.3", + "FluentAvaloniaUI": "1.1.5", + "Flurl.Http": "3.2.0", + "Live.Avalonia": "1.3.1", + "Material.Icons.Avalonia": "1.0.2", + "Splat.Ninject": "13.1.30" + } + }, + "artemis.ui.shared": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Avalonia": "0.10.10", + "Avalonia.ReactiveUI": "0.10.10", + "Avalonia.Svg.Skia": "0.10.8.3", + "Avalonia.Xaml.Behaviors": "0.10.10", + "Avalonia.Xaml.Interactions": "0.10.10", + "Avalonia.Xaml.Interactivity": "0.10.10", + "FluentAvaloniaUI": "1.1.5", + "Material.Icons.Avalonia": "1.0.2" + } + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/App.axaml b/src/Avalonia/Artemis.UI/App.axaml similarity index 90% rename from src/Artemis.UI.Avalonia/App.axaml rename to src/Avalonia/Artemis.UI/App.axaml index db7e15f0f..320593eef 100644 --- a/src/Artemis.UI.Avalonia/App.axaml +++ b/src/Avalonia/Artemis.UI/App.axaml @@ -1,10 +1,10 @@ + xmlns:ui="clr-namespace:Artemis.UI" + x:Class="Artemis.UI.App"> - + diff --git a/src/Avalonia/Artemis.UI/App.axaml.cs b/src/Avalonia/Artemis.UI/App.axaml.cs new file mode 100644 index 000000000..0572fda7c --- /dev/null +++ b/src/Avalonia/Artemis.UI/App.axaml.cs @@ -0,0 +1,54 @@ +using Artemis.Core.Ninject; +using Artemis.UI.Ninject; +using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Shared.Ninject; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Threading; +using FluentAvalonia.Styling; +using Ninject; +using ReactiveUI; +using Splat.Ninject; + +namespace Artemis.UI +{ + public class App : Application + { + private StandardKernel _kernel = null!; + + public override void Initialize() + { + InitializeNinject(); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = _kernel.Get() + }; + AvaloniaLocator.Current.GetService().ForceNativeTitleBarToTheme(desktop.MainWindow, "Dark"); + } + + base.OnFrameworkInitializationCompleted(); + } + + private void InitializeNinject() + { + _kernel = new StandardKernel(); + _kernel.Settings.InjectNonPublic = true; + + _kernel.Load(); + _kernel.Load(); + _kernel.Load(); + + _kernel.UseNinjectDependencyResolver(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings b/src/Avalonia/Artemis.UI/Artemis.UI.Avalonia.csproj.DotSettings similarity index 100% rename from src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj.DotSettings rename to src/Avalonia/Artemis.UI/Artemis.UI.Avalonia.csproj.DotSettings diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj new file mode 100644 index 000000000..76d126dc2 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -0,0 +1,45 @@ + + + Library + net5.0 + enable + bin\ + + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings b/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings new file mode 100644 index 000000000..452c5acb6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Assets/Images/Logo/bow-black.ico b/src/Avalonia/Artemis.UI/Assets/Images/Logo/bow-black.ico similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/Images/Logo/bow-black.ico rename to src/Avalonia/Artemis.UI/Assets/Images/Logo/bow-black.ico diff --git a/src/Artemis.UI.Avalonia/Assets/Images/Logo/bow-white.ico b/src/Avalonia/Artemis.UI/Assets/Images/Logo/bow-white.ico similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/Images/Logo/bow-white.ico rename to src/Avalonia/Artemis.UI/Assets/Images/Logo/bow-white.ico diff --git a/src/Artemis.UI.Avalonia/Assets/Images/Logo/bow-white.svg b/src/Avalonia/Artemis.UI/Assets/Images/Logo/bow-white.svg similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/Images/Logo/bow-white.svg rename to src/Avalonia/Artemis.UI/Assets/Images/Logo/bow-white.svg diff --git a/src/Artemis.UI.Avalonia/Assets/Images/Logo/bow.ico b/src/Avalonia/Artemis.UI/Assets/Images/Logo/bow.ico similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/Images/Logo/bow.ico rename to src/Avalonia/Artemis.UI/Assets/Images/Logo/bow.ico diff --git a/src/Artemis.UI.Avalonia/Assets/Images/Logo/bow.svg b/src/Avalonia/Artemis.UI/Assets/Images/Logo/bow.svg similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/Images/Logo/bow.svg rename to src/Avalonia/Artemis.UI/Assets/Images/Logo/bow.svg diff --git a/src/Artemis.UI.Avalonia/Assets/Images/home-banner.png b/src/Avalonia/Artemis.UI/Assets/Images/home-banner.png similarity index 100% rename from src/Artemis.UI.Avalonia/Assets/Images/home-banner.png rename to src/Avalonia/Artemis.UI/Assets/Images/home-banner.png diff --git a/src/Avalonia/Artemis.UI/Assets/avalonia-logo.ico b/src/Avalonia/Artemis.UI/Assets/avalonia-logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8d49ff9b94e52778f5324a1b87dd443a698b57 GIT binary patch literal 176111 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 zds⪻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

r@R8DskCg$QY%d=x6zD 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

!LxQh!NmF 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 /// Converts into . diff --git a/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs b/src/Avalonia/Artemis.UI/Converters/EnumToCollectionConverter.cs similarity index 97% rename from src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs rename to src/Avalonia/Artemis.UI/Converters/EnumToCollectionConverter.cs index a3e6609d1..4d64bc484 100644 --- a/src/Artemis.UI.Avalonia/Converters/EnumToCollectionConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/EnumToCollectionConverter.cs @@ -6,7 +6,7 @@ using System.Linq; using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Avalonia.Converters +namespace Artemis.UI.Converters { public class EnumToCollectionConverter : MarkupExtension, IValueConverter { diff --git a/src/Artemis.UI.Avalonia/Converters/LedIdToStringConverter.cs b/src/Avalonia/Artemis.UI/Converters/LedIdToStringConverter.cs similarity index 94% rename from src/Artemis.UI.Avalonia/Converters/LedIdToStringConverter.cs rename to src/Avalonia/Artemis.UI/Converters/LedIdToStringConverter.cs index 1cfe6b6d5..2160662ff 100644 --- a/src/Artemis.UI.Avalonia/Converters/LedIdToStringConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/LedIdToStringConverter.cs @@ -3,7 +3,7 @@ using System.Globalization; using Avalonia.Data.Converters; using RGB.NET.Core; -namespace Artemis.UI.Avalonia.Converters +namespace Artemis.UI.Converters { public class LedIdToStringConverter : IValueConverter { diff --git a/src/Artemis.UI.Avalonia/Converters/NormalizedPercentageConverter.cs b/src/Avalonia/Artemis.UI/Converters/NormalizedPercentageConverter.cs similarity index 94% rename from src/Artemis.UI.Avalonia/Converters/NormalizedPercentageConverter.cs rename to src/Avalonia/Artemis.UI/Converters/NormalizedPercentageConverter.cs index 5c03d09d3..8a653a026 100644 --- a/src/Artemis.UI.Avalonia/Converters/NormalizedPercentageConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/NormalizedPercentageConverter.cs @@ -2,7 +2,7 @@ using System.Globalization; using Avalonia.Data.Converters; -namespace Artemis.UI.Avalonia.Converters +namespace Artemis.UI.Converters { public class NormalizedPercentageConverter : IValueConverter { diff --git a/src/Artemis.UI.Avalonia/Converters/UriToFileNameConverter.cs b/src/Avalonia/Artemis.UI/Converters/UriToFileNameConverter.cs similarity index 93% rename from src/Artemis.UI.Avalonia/Converters/UriToFileNameConverter.cs rename to src/Avalonia/Artemis.UI/Converters/UriToFileNameConverter.cs index d8b34e3f3..b03483174 100644 --- a/src/Artemis.UI.Avalonia/Converters/UriToFileNameConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/UriToFileNameConverter.cs @@ -3,7 +3,7 @@ using System.Globalization; using System.IO; using Avalonia.Data.Converters; -namespace Artemis.UI.Avalonia.Converters +namespace Artemis.UI.Converters { public class UriToFileNameConverter : IValueConverter { diff --git a/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs b/src/Avalonia/Artemis.UI/Converters/ValuesAdditionConverter.cs similarity index 91% rename from src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs rename to src/Avalonia/Artemis.UI/Converters/ValuesAdditionConverter.cs index 55fe27a36..529704fe0 100644 --- a/src/Artemis.UI.Avalonia/Converters/ValuesAdditionConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/ValuesAdditionConverter.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Linq; using Avalonia.Data.Converters; -namespace Artemis.UI.Avalonia.Converters +namespace Artemis.UI.Converters { public class ValuesAdditionConverter : IMultiValueConverter { diff --git a/src/Artemis.UI.Avalonia/Exceptions/ArtemisGraphicsContextException.cs b/src/Avalonia/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs similarity index 92% rename from src/Artemis.UI.Avalonia/Exceptions/ArtemisGraphicsContextException.cs rename to src/Avalonia/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs index dca0fd609..653d65ef4 100644 --- a/src/Artemis.UI.Avalonia/Exceptions/ArtemisGraphicsContextException.cs +++ b/src/Avalonia/Artemis.UI/Exceptions/ArtemisGraphicsContextException.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Avalonia.Exceptions +namespace Artemis.UI.Exceptions { public class ArtemisGraphicsContextException : Exception { diff --git a/src/Artemis.UI.Avalonia/Exceptions/ArtemisUIException.cs b/src/Avalonia/Artemis.UI/Exceptions/ArtemisUIException.cs similarity index 89% rename from src/Artemis.UI.Avalonia/Exceptions/ArtemisUIException.cs rename to src/Avalonia/Artemis.UI/Exceptions/ArtemisUIException.cs index df8b6464c..7d5638eda 100644 --- a/src/Artemis.UI.Avalonia/Exceptions/ArtemisUIException.cs +++ b/src/Avalonia/Artemis.UI/Exceptions/ArtemisUIException.cs @@ -1,6 +1,6 @@ using System; -namespace Artemis.UI.Avalonia.Exceptions +namespace Artemis.UI.Exceptions { public class ArtemisUIException : Exception { diff --git a/src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs b/src/Avalonia/Artemis.UI/Extensions/BindableCollectionExtensions.cs similarity index 94% rename from src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs rename to src/Avalonia/Artemis.UI/Extensions/BindableCollectionExtensions.cs index b81330ef7..869487c2b 100644 --- a/src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs +++ b/src/Avalonia/Artemis.UI/Extensions/BindableCollectionExtensions.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -namespace Artemis.UI.Avalonia.Extensions +namespace Artemis.UI.Extensions { public static class ObservableCollectionExtensions { diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml b/src/Avalonia/Artemis.UI/MainWindow.axaml similarity index 95% rename from src/Artemis.UI.Avalonia/MainWindow.axaml rename to src/Avalonia/Artemis.UI/MainWindow.axaml index e24aa179c..f0cd1495a 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.MainWindow" + x:Class="Artemis.UI.MainWindow" Icon="/Assets/Images/Logo/bow.ico" Title="Artemis.UI.Avalonia" ExtendClientAreaToDecorationsHint="True" diff --git a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs similarity index 82% rename from src/Artemis.UI.Avalonia/MainWindow.axaml.cs rename to src/Avalonia/Artemis.UI/MainWindow.axaml.cs index b6c98ac3f..c49ecd415 100644 --- a/src/Artemis.UI.Avalonia/MainWindow.axaml.cs +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs @@ -1,9 +1,9 @@ -using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Artemis.UI.Screens.Root.ViewModels; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia +namespace Artemis.UI { public class MainWindow : ReactiveWindow { diff --git a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs similarity index 82% rename from src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs rename to src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index cef934c9a..b8e08da13 100644 --- a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,15 +1,14 @@ using System.Collections.ObjectModel; using Artemis.Core; -using Artemis.UI.Avalonia.Screens.Device; -using Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels; -using Artemis.UI.Avalonia.Screens.Device.ViewModels; -using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; -using Artemis.UI.Avalonia.Screens.Root.ViewModels; -using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; -using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels; +using Artemis.UI.Screens.Device.Tabs.ViewModels; +using Artemis.UI.Screens.Device.ViewModels; +using Artemis.UI.Screens.Plugins.ViewModels; +using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Screens.Settings.Tabs.ViewModels; +using Artemis.UI.Screens.SurfaceEditor.ViewModels; using ReactiveUI; -namespace Artemis.UI.Avalonia.Ninject.Factories +namespace Artemis.UI.Ninject.Factories { public interface IVmFactory { diff --git a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs similarity index 88% rename from src/Artemis.UI.Avalonia/Ninject/UIModule.cs rename to src/Avalonia/Artemis.UI/Ninject/UIModule.cs index faf29a400..30f910279 100644 --- a/src/Artemis.UI.Avalonia/Ninject/UIModule.cs +++ b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs @@ -1,13 +1,13 @@ using System; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Screens; -using Artemis.UI.Avalonia.Services.Interfaces; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; using Ninject.Extensions.Conventions; using Ninject.Modules; using Ninject.Planning.Bindings.Resolvers; -namespace Artemis.UI.Avalonia.Ninject +namespace Artemis.UI.Ninject { public class UIModule : NinjectModule { diff --git a/src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs b/src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs similarity index 72% rename from src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs rename to src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs index ca50b3734..72a8d394b 100644 --- a/src/Artemis.UI.Avalonia/Providers/AvaloniaInputProvider.cs +++ b/src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs @@ -1,6 +1,6 @@ using Artemis.Core.Services; -namespace Artemis.UI.Avalonia.Providers +namespace Artemis.UI.Providers { public class AvaloniaInputProvider : InputProvider { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml similarity index 90% rename from src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml rename to src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml index 1f90a6a68..1cbf586d4 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" - x:Class="Artemis.UI.Avalonia.Screens.Debugger.DebugView" + x:Class="Artemis.UI.Screens.Debugger.DebugView" Title="Artemis | Debugger" Width="1200" Height="800" diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs similarity index 94% rename from src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs index dc5c68597..fc050bc5c 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs @@ -1,6 +1,6 @@ using System; using System.Reactive.Disposables; -using Artemis.UI.Avalonia.Shared.Events; +using Artemis.UI.Shared.Events; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -8,7 +8,7 @@ using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger +namespace Artemis.UI.Screens.Debugger { public class DebugView : ReactiveWindow { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs similarity index 86% rename from src/Artemis.UI.Avalonia/Screens/Debugger/DebugViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs index 04e7fdc2a..cbfb2b1c6 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/DebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs @@ -1,17 +1,17 @@ using System; using System.Reactive.Disposables; -using Artemis.UI.Avalonia.Screens.Debugger.Tabs.DataModel; -using Artemis.UI.Avalonia.Screens.Debugger.Tabs.Logs; -using Artemis.UI.Avalonia.Screens.Debugger.Tabs.Performance; -using Artemis.UI.Avalonia.Screens.Debugger.Tabs.Render; -using Artemis.UI.Avalonia.Services.Interfaces; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Screens.Debugger.Tabs.DataModel; +using Artemis.UI.Screens.Debugger.Tabs.Logs; +using Artemis.UI.Screens.Debugger.Tabs.Performance; +using Artemis.UI.Screens.Debugger.Tabs.Render; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; using FluentAvalonia.UI.Controls; using Ninject; using Ninject.Parameters; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger +namespace Artemis.UI.Screens.Debugger { public class DebugViewModel : ActivatableViewModelBase, IScreen { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml similarity index 80% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml index d82578a19..dc8892cc8 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Debugger.Tabs.DataModel.DataModelDebugView"> + x:Class="Artemis.UI.Screens.Debugger.Tabs.DataModel.DataModelDebugView"> Data Model \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs index edc26496f..47c67b6f7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.DataModel +namespace Artemis.UI.Screens.Debugger.Tabs.DataModel { public class DataModelDebugView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs similarity index 77% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs index f5da16218..c2a09e345 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs @@ -1,7 +1,7 @@ -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.DataModel +namespace Artemis.UI.Screens.Debugger.Tabs.DataModel { public class DataModelDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml similarity index 82% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml index ffd33b22d..6a5ef6c81 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Debugger.Tabs.Logs.LogsDebugView"> + x:Class="Artemis.UI.Screens.Debugger.Tabs.Logs.LogsDebugView"> Logs \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs index d2cd7b97c..6d3a3dae7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.Logs +namespace Artemis.UI.Screens.Debugger.Tabs.Logs { public class LogsDebugView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs similarity index 77% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs index 01eb8348f..a8356a48c 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Logs/LogsDebugViewModel.cs @@ -1,7 +1,7 @@ -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.Logs +namespace Artemis.UI.Screens.Debugger.Tabs.Logs { public class LogsDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml similarity index 80% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml index 8641a964d..6659bfe19 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Debugger.Tabs.Performance.PerformanceDebugView"> + x:Class="Artemis.UI.Screens.Debugger.Tabs.Performance.PerformanceDebugView"> Performance \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs similarity index 84% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs index ed3608aae..d81eb0c80 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.Performance +namespace Artemis.UI.Screens.Debugger.Tabs.Performance { public class PerformanceDebugView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs similarity index 77% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs index 21ade068f..06aae723f 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs @@ -1,7 +1,7 @@ -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.Performance +namespace Artemis.UI.Screens.Debugger.Tabs.Performance { public class PerformanceDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml similarity index 88% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugView.axaml rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml index ef59cd8fc..34057dda9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Debugger.Tabs.Render.RenderDebugView"> + x:Class="Artemis.UI.Screens.Debugger.Tabs.Render.RenderDebugView"> diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs index 97da511c8..0c90e2fb9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.Render +namespace Artemis.UI.Screens.Debugger.Tabs.Render { public class RenderDebugView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs similarity index 97% rename from src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs index 6f58feab8..9aaca4855 100644 --- a/src/Artemis.UI.Avalonia/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs @@ -3,11 +3,11 @@ using System.Reactive.Disposables; using System.Timers; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using ReactiveUI; using SkiaSharp; -namespace Artemis.UI.Avalonia.Screens.Debugger.Tabs.Render +namespace Artemis.UI.Screens.Debugger.Tabs.Render { public class RenderDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs similarity index 86% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs index bd93e2113..c2a96146d 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs @@ -1,11 +1,11 @@ using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using Avalonia; using RGB.NET.Core; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs.ViewModels { public class DeviceInfoTabViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs similarity index 96% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs index 212148ceb..1987a2894 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs @@ -4,11 +4,11 @@ using System.Collections.Specialized; using System.Linq; using System.Reactive.Disposables; using Artemis.Core; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using DynamicData.Binding; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs.ViewModels { public class DeviceLedsTabViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs similarity index 97% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs index 323b1c432..a94898182 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs @@ -4,13 +4,13 @@ using System.ComponentModel; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Builders; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; using SkiaSharp; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs.ViewModels { public class DevicePropertiesTabViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs similarity index 96% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs index 095f04dae..3f51b9e70 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs @@ -4,12 +4,12 @@ using System.Collections.Specialized; using System.Linq; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Exceptions; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Exceptions; +using Artemis.UI.Shared; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs.ViewModels { public class InputMappingsTabViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml similarity index 98% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml index 18d3375c6..3541903cf 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Device.Tabs.Views.DeviceInfoTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.Views.DeviceInfoTabView"> diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs index dae2666b3..b08fabd45 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs.Views { public partial class DeviceInfoTabView : UserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml similarity index 81% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml index ab528dee6..f3cef6e32 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Device.Tabs.Views.DeviceLedsTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.Views.DeviceLedsTabView"> Welcome to Avalonia! diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs index e3dbd1d2f..e0057260e 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs.Views { public partial class DeviceLedsTabView : UserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml similarity index 97% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml index 52e8643bb..de3a7059c 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml @@ -4,10 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:converters="clr-namespace:Artemis.UI.Avalonia.Shared.Converters;assembly=Artemis.UI.Avalonia.Shared" - xmlns:viewModels="clr-namespace:Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels" + xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" + xmlns:viewModels="clr-namespace:Artemis.UI.Screens.Device.Tabs.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1200" - x:Class="Artemis.UI.Avalonia.Screens.Device.Tabs.Views.DevicePropertiesTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.Views.DevicePropertiesTabView"> diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs similarity index 83% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs index 63d0295a1..31e29cbb3 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs @@ -1,9 +1,9 @@ -using Artemis.UI.Avalonia.Screens.Device.Tabs.ViewModels; +using Artemis.UI.Screens.Device.Tabs.ViewModels; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs.Views { public partial class DevicePropertiesTabView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/InputMappingsTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml similarity index 80% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/InputMappingsTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml index c18ce5319..9cb5e59c8 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/InputMappingsTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Device.Tabs.Views.InputMappingsTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.Views.InputMappingsTabView"> Welcome to Avalonia! diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs index 21ea6cf5b..53c7dc6ad 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Avalonia.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs.Views { public partial class InputMappingsTabView : UserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs similarity index 91% rename from src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs index 8eb1de492..a26178751 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs @@ -4,13 +4,13 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Builders; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Avalonia.Screens.Device.ViewModels +namespace Artemis.UI.Screens.Device.ViewModels { public class DeviceDetectInputViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DevicePropertiesViewModel.cs similarity index 89% rename from src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DevicePropertiesViewModel.cs index 1acc67272..4ce089f9b 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DevicePropertiesViewModel.cs @@ -1,11 +1,11 @@ using System.Collections.ObjectModel; using Artemis.Core; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared; using RGB.NET.Core; using ArtemisLed = Artemis.Core.ArtemisLed; -namespace Artemis.UI.Avalonia.Screens.Device +namespace Artemis.UI.Screens.Device.ViewModels { public class DevicePropertiesViewModel : DialogViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceSettingsViewModel.cs similarity index 93% rename from src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceSettingsViewModel.cs index f724a068f..db9a9f4f3 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DeviceSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceSettingsViewModel.cs @@ -2,16 +2,16 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Settings.Tabs.ViewModels; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Threading; using Humanizer; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Avalonia.Screens.Device.ViewModels +namespace Artemis.UI.Screens.Device.ViewModels { public class DeviceSettingsViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml similarity index 94% rename from src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml index 113d6d9b8..760e110ec 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="1050" - x:Class="Artemis.UI.Avalonia.Screens.Device.Views.DeviceDetectInputView"> + x:Class="Artemis.UI.Screens.Device.Views.DeviceDetectInputView"> diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml.cs similarity index 77% rename from src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml.cs index cd60e9293..c36de345a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceDetectInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml.cs @@ -1,8 +1,8 @@ -using Artemis.UI.Avalonia.Screens.Device.ViewModels; +using Artemis.UI.Screens.Device.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Device.Views +namespace Artemis.UI.Screens.Device.Views { public class DeviceDetectInputView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml similarity index 95% rename from src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml index fbb52cff7..5d8fbd58a 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml @@ -2,9 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" - x:Class="Artemis.UI.Avalonia.Screens.Device.DevicePropertiesView" + x:Class="Artemis.UI.Screens.Device.Views.DevicePropertiesView" Title="Artemis | Device Properties" Width="1250" Height="900" diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml.cs similarity index 83% rename from src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml.cs index 7fce03fb9..624fc567c 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml.cs @@ -1,8 +1,9 @@ +using Artemis.UI.Screens.Device.ViewModels; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Device +namespace Artemis.UI.Screens.Device.Views { public partial class DevicePropertiesView : ReactiveWindow { diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml similarity index 93% rename from src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml index b1b8e5ae1..d36be0a13 100644 --- a/src/Artemis.UI.Avalonia/Screens/Device/Views/DeviceSettingsView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml @@ -2,11 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Device.Views.DeviceSettingsView"> + x:Class="Artemis.UI.Screens.Device.Views.DeviceSettingsView"> @@ -23,7 +23,7 @@ - + x:Class="Artemis.UI.Screens.Home.Views.HomeView"> { diff --git a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs b/src/Avalonia/Artemis.UI/Screens/MainScreenViewModel.cs similarity index 85% rename from src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/MainScreenViewModel.cs index 12502fb41..96713b1fe 100644 --- a/src/Artemis.UI.Avalonia/Screens/MainScreenViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/MainScreenViewModel.cs @@ -1,7 +1,7 @@ -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens +namespace Artemis.UI.Screens { public abstract class MainScreenViewModel : ActivatableViewModelBase, IRoutableViewModel { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs similarity index 97% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs index a40ceb2e4..fdba3ed41 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Builders; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginFeatureViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs similarity index 76% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs index c6d02c726..d2ef14784 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs @@ -1,7 +1,7 @@ using Artemis.Core; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginPrerequisiteActionViewModel : ViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs similarity index 97% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs index 7f46538d0..c7aa6e392 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs @@ -4,10 +4,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginPrerequisiteViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs similarity index 95% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs index af0818afd..f4960bb9c 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs @@ -5,12 +5,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs similarity index 96% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs index 238cd309e..146712b13 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs @@ -6,12 +6,12 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginPrerequisitesUninstallDialogViewModel : DialogViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs similarity index 98% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs index 9ae740a19..a37561ba7 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs @@ -1,22 +1,21 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.IO; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Exceptions; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Shared; -using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Artemis.UI.Exceptions; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Threading; using Ninject; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginSettingsViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs similarity index 86% rename from src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs index a2d08fde6..78118b2a0 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs @@ -1,8 +1,8 @@ using System; using Artemis.Core; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; -namespace Artemis.UI.Avalonia.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.ViewModels { public class PluginSettingsWindowViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml similarity index 92% rename from src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml rename to src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml index 4f990e857..d9d938ae1 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginFeatureView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml @@ -2,10 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginFeatureView"> + x:Class="Artemis.UI.Screens.Plugins.Views.PluginFeatureView"> @@ -23,7 +23,7 @@ - { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml similarity index 96% rename from src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml rename to src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml index 20feaa0ae..3d80c7ab8 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml @@ -4,13 +4,13 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:shared="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" + xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginSettingsView"> + x:Class="Artemis.UI.Screens.Plugins.Views.PluginSettingsView"> - { diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml similarity index 89% rename from src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml rename to src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml index 2676fac81..82a8d3393 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Plugins.Views.PluginSettingsWindowView" + x:Class="Artemis.UI.Screens.Plugins.Views.PluginSettingsWindowView" Title="{Binding DisplayName}" ExtendClientAreaToDecorationsHint="True" Width="800" diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs similarity index 90% rename from src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs index 97e075407..fb152bbf9 100644 --- a/src/Artemis.UI.Avalonia/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs @@ -1,13 +1,13 @@ using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using Artemis.UI.Avalonia.Screens.Plugins.ViewModels; +using Artemis.UI.Screens.Plugins.ViewModels; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Plugins.Views +namespace Artemis.UI.Screens.Plugins.Views { public class PluginSettingsWindowView : ReactiveWindow { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs new file mode 100644 index 000000000..5ebeb327d --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs @@ -0,0 +1,8 @@ +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.ProfileEditor.ViewModels +{ + public class ProfileEditorViewModel : ActivatableViewModelBase + { + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml similarity index 81% rename from src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml index 1db8610af..7e808d336 100644 --- a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.ProfileEditor.Views.ProfileEditorView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Views.ProfileEditorView"> Welcome to Avalonia! diff --git a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs similarity index 74% rename from src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs index 5dc3141ed..ca7b4bb7a 100644 --- a/src/Artemis.UI.Avalonia/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs @@ -1,8 +1,8 @@ -using Artemis.UI.Avalonia.Screens.ProfileEditor.ViewModels; +using Artemis.UI.Screens.ProfileEditor.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.ProfileEditor.Views +namespace Artemis.UI.Screens.ProfileEditor.Views { public class ProfileEditorView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/RootViewModel.cs similarity index 79% rename from src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Root/ViewModels/RootViewModel.cs index 7edbfc0f7..21b895560 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/RootViewModel.cs @@ -1,10 +1,10 @@ using Artemis.Core.Services; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Services.Interfaces; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +namespace Artemis.UI.Screens.Root.ViewModels { public class RootViewModel : ActivatableViewModelBase, IScreen { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarCategoryViewModel.cs similarity index 94% rename from src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarCategoryViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarCategoryViewModel.cs index 2ac8aabe7..ff167f789 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarCategoryViewModel.cs @@ -2,11 +2,11 @@ using System.Linq; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +namespace Artemis.UI.Screens.Root.ViewModels { public class SidebarCategoryViewModel : ViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs similarity index 91% rename from src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs index 71194c5f2..7a61fa3de 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarProfileConfigurationViewModel.cs @@ -1,8 +1,8 @@ using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; -namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +namespace Artemis.UI.Screens.Root.ViewModels { public class SidebarProfileConfigurationViewModel : ViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarScreenViewModel.cs similarity index 92% rename from src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarScreenViewModel.cs index cc66c782d..5d165b912 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarScreenViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarScreenViewModel.cs @@ -1,11 +1,11 @@ using System; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Shared; using Material.Icons; using Ninject; using Ninject.Parameters; using ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +namespace Artemis.UI.Screens.Root.ViewModels { public class SidebarScreenViewModel : SidebarScreenViewModel where T : MainScreenViewModel { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarViewModel.cs similarity index 91% rename from src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarViewModel.cs index 60a6e36bc..20e7fa5a2 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/SidebarViewModel.cs @@ -4,18 +4,18 @@ using System.Linq; using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Avalonia.Ninject.Factories; -using Artemis.UI.Avalonia.Screens.Home.ViewModels; -using Artemis.UI.Avalonia.Screens.Settings; -using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels; -using Artemis.UI.Avalonia.Screens.Workshop.ViewModels; -using Artemis.UI.Avalonia.Shared; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Home.ViewModels; +using Artemis.UI.Screens.Settings; +using Artemis.UI.Screens.SurfaceEditor.ViewModels; +using Artemis.UI.Screens.Workshop.ViewModels; +using Artemis.UI.Shared; using Material.Icons; using Ninject; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Avalonia.Screens.Root.ViewModels +namespace Artemis.UI.Screens.Root.ViewModels { public class SidebarViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml similarity index 93% rename from src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml rename to src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml index 7b35385ee..c2bab7563 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Root.Views.RootView"> + x:Class="Artemis.UI.Screens.Root.Views.RootView"> diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml.cs similarity index 76% rename from src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml.cs index 547f39877..410cd3174 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml.cs @@ -1,8 +1,8 @@ -using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Artemis.UI.Screens.Root.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Avalonia.Screens.Root.Views +namespace Artemis.UI.Screens.Root.Views { public class RootView : ReactiveUserControl { diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Views/SidebarCategoryView.axaml similarity index 97% rename from src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml rename to src/Avalonia/Artemis.UI/Screens/Root/Views/SidebarCategoryView.axaml index d6d3b7633..4fe165ef4 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/SidebarCategoryView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Views/SidebarCategoryView.axaml @@ -3,9 +3,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:local="clr-namespace:Artemis.UI.Avalonia.Screens.Root.ViewModels" + xmlns:local="clr-namespace:Artemis.UI.Screens.Root.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Avalonia.Screens.Root.Views.SidebarCategoryView"> + x:Class="Artemis.UI.Screens.Root.Views.SidebarCategoryView"> + + + + + + + + + Rendering + + + + + + + + Data Model + + + + + + + + Performance + + + + + + + + Logging + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs index cbfb2b1c6..39f75b565 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs @@ -4,6 +4,7 @@ using Artemis.UI.Screens.Debugger.Tabs.DataModel; using Artemis.UI.Screens.Debugger.Tabs.Logs; using Artemis.UI.Screens.Debugger.Tabs.Performance; using Artemis.UI.Screens.Debugger.Tabs.Render; +using Artemis.UI.Screens.Debugger.Tabs.Settings; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using FluentAvalonia.UI.Controls; @@ -50,20 +51,23 @@ namespace Artemis.UI.Screens.Debugger { // Kind of a lame way to do this but it's so static idc ConstructorArgument hostScreen = new("hostScreen", this); - switch ((string) item.Content) + switch (item.Tag as string) { case "Rendering": Router.Navigate.Execute(_kernel.Get(hostScreen)); break; - case "Logs": - Router.Navigate.Execute(_kernel.Get(hostScreen)); - break; - case "Data Model": + case "DataModel": Router.Navigate.Execute(_kernel.Get(hostScreen)); break; case "Performance": Router.Navigate.Execute(_kernel.Get(hostScreen)); break; + case "Logging": + Router.Navigate.Execute(_kernel.Get(hostScreen)); + break; + case "Settings": + Router.Navigate.Execute(_kernel.Get(hostScreen)); + break; } } diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml index dc8892cc8..564aee31f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml @@ -2,7 +2,109 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared;assembly=Artemis.UI.Shared" + xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Debugger.Tabs.DataModel.DataModelDebugView"> - Data Model + + + + + + + + + + + + + [ + + ] + + + + + + + + + + + [ + + ] + + + + + + + + + + + [ + + ] + + + + + + + + + + + + + [ + + ] + + + List item [ + + ] + + + + + + + + + + + [ + + ] + + + + + + + + + List item [ + + ] + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs index c2a09e345..734634975 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs @@ -1,16 +1,147 @@ -using Artemis.UI.Shared; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Timers; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Artemis.UI.Shared.DataModelVisualization.Shared; +using Artemis.UI.Shared.Services; +using DynamicData; using ReactiveUI; namespace Artemis.UI.Screens.Debugger.Tabs.DataModel { public class DataModelDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { - public DataModelDebugViewModel(IScreen hostScreen) + private readonly IDataModelUIService _dataModelUIService; + private readonly IPluginManagementService _pluginManagementService; + private readonly Timer _updateTimer; + + private bool _isModuleFilterEnabled; + private DataModelPropertiesViewModel? _mainDataModel; + private string? _propertySearch; + private Module? _selectedModule; + private bool _slowUpdates; + + public DataModelDebugViewModel(IScreen hostScreen, IDataModelUIService dataModelUIService, IPluginManagementService pluginManagementService) { + _dataModelUIService = dataModelUIService; + _pluginManagementService = pluginManagementService; + _updateTimer = new Timer(25); + _updateTimer.Elapsed += UpdateTimerOnElapsed; + HostScreen = hostScreen; + Modules = new ObservableCollection(); + + this.WhenActivated(disposables => + { + Observable.FromEventPattern(x => pluginManagementService.PluginFeatureEnabled += x, x => pluginManagementService.PluginFeatureEnabled -= x) + .Subscribe(d => PluginFeatureToggled(d.EventArgs.PluginFeature)) + .DisposeWith(disposables); + Observable.FromEventPattern(x => pluginManagementService.PluginFeatureDisabled += x, x => pluginManagementService.PluginFeatureDisabled -= x) + .Subscribe(d => PluginFeatureToggled(d.EventArgs.PluginFeature)) + .DisposeWith(disposables); + + GetDataModel(); + _updateTimer.Start(); + Disposable.Create(() => _updateTimer.Stop()).DisposeWith(disposables); + }); } public string UrlPathSegment => "data-model"; public IScreen HostScreen { get; } + + public DataModelPropertiesViewModel? MainDataModel + { + get => _mainDataModel; + set => this.RaiseAndSetIfChanged(ref _mainDataModel, value); + } + + public string? PropertySearch + { + get => _propertySearch; + set => this.RaiseAndSetIfChanged(ref _propertySearch, value); + } + + public bool SlowUpdates + { + get => _slowUpdates; + set + { + this.RaiseAndSetIfChanged(ref _slowUpdates, value); + _updateTimer.Interval = _slowUpdates ? 500 : 25; + } + } + + public ObservableCollection Modules { get; } + + public Module? SelectedModule + { + get => _selectedModule; + set + { + this.RaiseAndSetIfChanged(ref _selectedModule, value); + GetDataModel(); + } + } + + public bool IsModuleFilterEnabled + { + get => _isModuleFilterEnabled; + set + { + this.RaiseAndSetIfChanged(ref _isModuleFilterEnabled, value); + + if (!IsModuleFilterEnabled) + SelectedModule = null; + else + GetDataModel(); + } + } + private void UpdateTimerOnElapsed(object sender, ElapsedEventArgs e) + { + if (MainDataModel == null) + return; + + lock (MainDataModel) + { + MainDataModel.Update(_dataModelUIService, new DataModelUpdateConfiguration(true)); + } + } + + private void PluginFeatureToggled(PluginFeature pluginFeature) + { + if (pluginFeature is Module) + PopulateModules(); + } + + private void GetDataModel() + { + MainDataModel = SelectedModule != null + ? _dataModelUIService.GetPluginDataModelVisualization(new List() { SelectedModule }, false) + : _dataModelUIService.GetMainDataModelVisualization(); + } + + private void PopulateModules() + { + Modules.Clear(); + Modules.AddRange(_pluginManagementService.GetFeaturesOfType().Where(p => p.IsEnabled).OrderBy(m => m.Info.Name)); + + if (MainDataModel == null) + return; + + if (SelectedModule == null) + { + if (MainDataModel != null) + _dataModelUIService.UpdateModules(MainDataModel); + } + else if (!SelectedModule.IsEnabled) + SelectedModule = null; + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml index 34057dda9..905d400d6 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml @@ -4,13 +4,26 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Debugger.Tabs.Render.RenderDebugView"> - - - - - - Render - - - + + Render + + On this page you can view what Artemis renders to devices in real time. Artemis will overlay this image on your devices, taking the average of each pixel covering a LED, resulting the image appearing on your devices. + + + Please note that having this window open can have a performance impact on your system. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs index 9aaca4855..316a79090 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs @@ -1,9 +1,15 @@ -using System.IO; +using System.Diagnostics; +using System.IO; using System.Reactive.Disposables; +using System.Reflection.Metadata.Ecma335; using System.Timers; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using Avalonia.Platform; using ReactiveUI; using SkiaSharp; @@ -12,11 +18,9 @@ namespace Artemis.UI.Screens.Debugger.Tabs.Render public class RenderDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { private readonly ICoreService _coreService; - private readonly Timer _fpsTimer; private double _currentFps; - private SKImage? _currentFrame; - private int _frames; + private Bitmap? _currentFrame; private string? _frameTargetPath; private string _renderer; private int _renderHeight; @@ -25,10 +29,7 @@ namespace Artemis.UI.Screens.Debugger.Tabs.Render public RenderDebugViewModel(DebugViewModel hostScreen, ICoreService coreService) { HostScreen = hostScreen; - _coreService = coreService; - _fpsTimer = new Timer(1000); - _fpsTimer.Start(); this.WhenActivated(disposables => { @@ -37,7 +38,7 @@ namespace Artemis.UI.Screens.Debugger.Tabs.Render }); } - public SKImage? CurrentFrame + public Bitmap? CurrentFrame { get => _currentFrame; set => this.RaiseAndSetIfChanged(ref _currentFrame, value); @@ -69,21 +70,18 @@ namespace Artemis.UI.Screens.Debugger.Tabs.Render private void HandleActivation() { + Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; _coreService.FrameRendered += CoreServiceOnFrameRendered; - _fpsTimer.Elapsed += FpsTimerOnElapsed; } private void HandleDeactivation() { _coreService.FrameRendered -= CoreServiceOnFrameRendered; - _fpsTimer.Elapsed -= FpsTimerOnElapsed; - _fpsTimer.Dispose(); } private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) { - _frames++; - + CurrentFps = _coreService.FrameRate; using SKImage skImage = e.Texture.Surface.Snapshot(); SKImageInfo bitmapInfo = e.Texture.ImageInfo; @@ -102,16 +100,14 @@ namespace Artemis.UI.Screens.Debugger.Tabs.Render RenderHeight = bitmapInfo.Height; RenderWidth = bitmapInfo.Width; - - CurrentFrame = e.Texture.Surface.Snapshot(); + + // TODO: This performs well enough but look into something else + using (SKData data = skImage.Encode(SKEncodedImageFormat.Png, 100)) + { + CurrentFrame = new Bitmap(data.AsStream()); + } } - private void FpsTimerOnElapsed(object sender, ElapsedEventArgs e) - { - CurrentFps = _frames; - Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; - _frames = 0; - } public string UrlPathSegment => "render"; public IScreen HostScreen { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsView.axaml new file mode 100644 index 000000000..ccdf809ea --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsView.axaml @@ -0,0 +1,8 @@ + + Settings + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsView.axaml.cs new file mode 100644 index 000000000..1880378e0 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Debugger.Tabs.Settings +{ + public class DebugSettingsView : ReactiveUserControl + { + public DebugSettingsView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsViewModel.cs new file mode 100644 index 000000000..1cd12164d --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Settings/DebugSettingsViewModel.cs @@ -0,0 +1,16 @@ +using Artemis.UI.Shared; +using ReactiveUI; + +namespace Artemis.UI.Screens.Debugger.Tabs.Settings +{ + public class DebugSettingsViewModel : ActivatableViewModelBase, IRoutableViewModel + { + public DebugSettingsViewModel(IScreen hostScreen) + { + HostScreen = hostScreen; + } + + public string UrlPathSegment => "logs"; + public IScreen HostScreen { get; } + } +} \ No newline at end of file From 091b65d1e4f79f4cf44e80c0ed466c30033bb413 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 21 Nov 2021 10:02:33 +0100 Subject: [PATCH 081/270] UI - Simplify folder structure to match old project better --- .../Events/DataModelInputDynamicEventArgs.cs | 21 ------------------- .../Events/DataModelInputStaticEventArgs.cs | 20 ------------------ .../Events/SelectionRectangleEventArgs.cs | 15 ------------- .../Artemis.UI/ArtemisBootstrapper.cs | 2 +- src/Avalonia/Artemis.UI/MainWindow.axaml.cs | 2 +- .../Ninject/Factories/IVMFactory.cs | 12 +++++------ .../{Views => }/DeviceDetectInputView.axaml | 2 +- .../DeviceDetectInputView.axaml.cs | 3 +-- .../DeviceDetectInputViewModel.cs | 2 +- .../{Views => }/DevicePropertiesView.axaml | 2 +- .../{Views => }/DevicePropertiesView.axaml.cs | 3 +-- .../DevicePropertiesViewModel.cs | 2 +- .../{Views => }/DeviceSettingsView.axaml | 2 +- .../{Views => }/DeviceSettingsView.axaml.cs | 2 +- .../DeviceSettingsViewModel.cs | 4 ++-- .../Tabs/{Views => }/DeviceInfoTabView.axaml | 2 +- .../{Views => }/DeviceInfoTabView.axaml.cs | 2 +- .../DeviceInfoTabViewModel.cs | 2 +- .../Tabs/{Views => }/DeviceLedsTabView.axaml | 2 +- .../{Views => }/DeviceLedsTabView.axaml.cs | 2 +- .../DeviceLedsTabViewModel.cs | 2 +- .../{Views => }/DevicePropertiesTabView.axaml | 6 +++--- .../DevicePropertiesTabView.axaml.cs | 3 +-- .../DevicePropertiesTabViewModel.cs | 2 +- .../{Views => }/InputMappingsTabView.axaml | 2 +- .../{Views => }/InputMappingsTabView.axaml.cs | 2 +- .../InputMappingsTabViewModel.cs | 2 +- .../Screens/Home/{Views => }/HomeView.axaml | 2 +- .../Home/{Views => }/HomeView.axaml.cs | 3 +-- .../Home/{ViewModels => }/HomeViewModel.cs | 2 +- ...uginPrerequisitesInstallDialogViewModel.cs | 2 +- ...inPrerequisitesUninstallDialogViewModel.cs | 2 +- .../{Views => }/PluginFeatureView.axaml | 2 +- .../{Views => }/PluginFeatureView.axaml.cs | 3 +-- .../PluginFeatureViewModel.cs | 3 ++- .../PluginPrerequisiteActionViewModel.cs | 2 +- .../PluginPrerequisiteViewModel.cs | 2 +- .../{Views => }/PluginSettingsView.axaml | 2 +- .../{Views => }/PluginSettingsView.axaml.cs | 3 +-- .../PluginSettingsViewModel.cs | 3 ++- .../PluginSettingsWindowView.axaml | 2 +- .../PluginSettingsWindowView.axaml.cs | 3 +-- .../PluginSettingsWindowViewModel.cs | 2 +- .../{Views => }/ProfileEditorView.axaml | 2 +- .../{Views => }/ProfileEditorView.axaml.cs | 3 +-- .../ProfileEditorViewModel.cs | 2 +- .../Screens/Root/{Views => }/RootView.axaml | 2 +- .../Root/{Views => }/RootView.axaml.cs | 3 +-- .../Root/{ViewModels => }/RootViewModel.cs | 3 ++- .../SidebarCategoryView.axaml | 4 ++-- .../SidebarCategoryView.axaml.cs | 5 ++--- .../SidebarCategoryViewModel.cs | 2 +- .../SidebarProfileConfigurationView.axaml | 2 +- .../SidebarProfileConfigurationView.axaml.cs | 2 +- .../SidebarProfileConfigurationViewModel.cs | 2 +- .../SidebarScreenView.axaml | 2 +- .../SidebarScreenView.axaml.cs | 2 +- .../SidebarScreenViewModel.cs | 2 +- .../Root/{Views => Sidebar}/SidebarView.axaml | 2 +- .../{Views => Sidebar}/SidebarView.axaml.cs | 5 ++--- .../SidebarViewModel.cs | 8 +++---- .../Screens/Settings/SettingsViewModel.cs | 2 +- .../Tabs/{Views => }/AboutTabView.axaml | 2 +- .../Tabs/{Views => }/AboutTabView.axaml.cs | 3 +-- .../{ViewModels => }/AboutTabViewModel.cs | 2 +- .../Tabs/{Views => }/DevicesTabView.axaml | 2 +- .../Tabs/{Views => }/DevicesTabView.axaml.cs | 3 +-- .../{ViewModels => }/DevicesTabViewModel.cs | 4 ++-- .../Tabs/{Views => }/GeneralTabView.axaml | 2 +- .../Tabs/{Views => }/GeneralTabView.axaml.cs | 3 +-- .../{ViewModels => }/GeneralTabViewModel.cs | 2 +- .../Tabs/{Views => }/PluginsTabView.axaml | 2 +- .../Tabs/{Views => }/PluginsTabView.axaml.cs | 3 +-- .../{ViewModels => }/PluginsTabViewModel.cs | 4 ++-- .../{Views => }/ListDeviceView.axaml | 2 +- .../{Views => }/ListDeviceView.axaml.cs | 2 +- .../{ViewModels => }/ListDeviceViewModel.cs | 2 +- .../{Views => }/SurfaceDeviceView.axaml | 2 +- .../{Views => }/SurfaceDeviceView.axaml.cs | 3 +-- .../SurfaceDeviceViewModel.cs | 2 +- .../{Views => }/SurfaceEditorView.axaml | 2 +- .../{Views => }/SurfaceEditorView.axaml.cs | 3 +-- .../SurfaceEditorViewModel.cs | 2 +- .../Workshop/{Views => }/WorkshopView.axaml | 2 +- .../{Views => }/WorkshopView.axaml.cs | 3 +-- .../{ViewModels => }/WorkshopViewModel.cs | 2 +- 86 files changed, 102 insertions(+), 173 deletions(-) delete mode 100644 src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs delete mode 100644 src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs delete mode 100644 src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs rename src/Avalonia/Artemis.UI/Screens/Device/{Views => }/DeviceDetectInputView.axaml (94%) rename src/Avalonia/Artemis.UI/Screens/Device/{Views => }/DeviceDetectInputView.axaml.cs (80%) rename src/Avalonia/Artemis.UI/Screens/Device/{ViewModels => }/DeviceDetectInputViewModel.cs (97%) rename src/Avalonia/Artemis.UI/Screens/Device/{Views => }/DevicePropertiesView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Device/{Views => }/DevicePropertiesView.axaml.cs (83%) rename src/Avalonia/Artemis.UI/Screens/Device/{ViewModels => }/DevicePropertiesViewModel.cs (96%) rename src/Avalonia/Artemis.UI/Screens/Device/{Views => }/DeviceSettingsView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Device/{Views => }/DeviceSettingsView.axaml.cs (88%) rename src/Avalonia/Artemis.UI/Screens/Device/{ViewModels => }/DeviceSettingsViewModel.cs (97%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/DeviceInfoTabView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/DeviceInfoTabView.axaml.cs (87%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{ViewModels => }/DeviceInfoTabViewModel.cs (95%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/DeviceLedsTabView.axaml (83%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/DeviceLedsTabView.axaml.cs (87%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{ViewModels => }/DeviceLedsTabViewModel.cs (97%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/DevicePropertiesTabView.axaml (97%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/DevicePropertiesTabView.axaml.cs (85%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{ViewModels => }/DevicePropertiesTabViewModel.cs (99%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/InputMappingsTabView.axaml (82%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{Views => }/InputMappingsTabView.axaml.cs (87%) rename src/Avalonia/Artemis.UI/Screens/Device/Tabs/{ViewModels => }/InputMappingsTabViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/Home/{Views => }/HomeView.axaml (99%) rename src/Avalonia/Artemis.UI/Screens/Home/{Views => }/HomeView.axaml.cs (79%) rename src/Avalonia/Artemis.UI/Screens/Home/{ViewModels => }/HomeViewModel.cs (83%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => Dialogs}/PluginPrerequisitesInstallDialogViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => Dialogs}/PluginPrerequisitesUninstallDialogViewModel.cs (99%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{Views => }/PluginFeatureView.axaml (97%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{Views => }/PluginFeatureView.axaml.cs (80%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => }/PluginFeatureViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => }/PluginPrerequisiteActionViewModel.cs (86%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => }/PluginPrerequisiteViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{Views => }/PluginSettingsView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{Views => }/PluginSettingsView.axaml.cs (80%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => }/PluginSettingsViewModel.cs (99%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{Views => }/PluginSettingsWindowView.axaml (90%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{Views => }/PluginSettingsWindowView.axaml.cs (91%) rename src/Avalonia/Artemis.UI/Screens/Plugins/{ViewModels => }/PluginSettingsWindowViewModel.cs (92%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/{Views => }/ProfileEditorView.axaml (82%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/{Views => }/ProfileEditorView.axaml.cs (77%) rename src/Avalonia/Artemis.UI/Screens/ProfileEditor/{ViewModels => }/ProfileEditorViewModel.cs (67%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => }/RootView.axaml (94%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => }/RootView.axaml.cs (79%) rename src/Avalonia/Artemis.UI/Screens/Root/{ViewModels => }/RootViewModel.cs (91%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarCategoryView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarCategoryView.axaml.cs (83%) rename src/Avalonia/Artemis.UI/Screens/Root/{ViewModels => Sidebar}/SidebarCategoryViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarProfileConfigurationView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarProfileConfigurationView.axaml.cs (89%) rename src/Avalonia/Artemis.UI/Screens/Root/{ViewModels => Sidebar}/SidebarProfileConfigurationViewModel.cs (95%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarScreenView.axaml (90%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarScreenView.axaml.cs (88%) rename src/Avalonia/Artemis.UI/Screens/Root/{ViewModels => Sidebar}/SidebarScreenViewModel.cs (95%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/Root/{Views => Sidebar}/SidebarView.axaml.cs (72%) rename src/Avalonia/Artemis.UI/Screens/Root/{ViewModels => Sidebar}/SidebarViewModel.cs (95%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/AboutTabView.axaml (99%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/AboutTabView.axaml.cs (77%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{ViewModels => }/AboutTabViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/DevicesTabView.axaml (93%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/DevicesTabView.axaml.cs (77%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{ViewModels => }/DevicesTabViewModel.cs (97%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/GeneralTabView.axaml (99%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/GeneralTabView.axaml.cs (77%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{ViewModels => }/GeneralTabViewModel.cs (99%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/PluginsTabView.axaml (89%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{Views => }/PluginsTabView.axaml.cs (77%) rename src/Avalonia/Artemis.UI/Screens/Settings/Tabs/{ViewModels => }/PluginsTabViewModel.cs (97%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{Views => }/ListDeviceView.axaml (83%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{Views => }/ListDeviceView.axaml.cs (86%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{ViewModels => }/ListDeviceViewModel.cs (92%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{Views => }/SurfaceDeviceView.axaml (95%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{Views => }/SurfaceDeviceView.axaml.cs (93%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{ViewModels => }/SurfaceDeviceViewModel.cs (98%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{Views => }/SurfaceEditorView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{Views => }/SurfaceEditorView.axaml.cs (96%) rename src/Avalonia/Artemis.UI/Screens/SurfaceEditor/{ViewModels => }/SurfaceEditorViewModel.cs (99%) rename src/Avalonia/Artemis.UI/Screens/Workshop/{Views => }/WorkshopView.axaml (95%) rename src/Avalonia/Artemis.UI/Screens/Workshop/{Views => }/WorkshopView.axaml.cs (78%) rename src/Avalonia/Artemis.UI/Screens/Workshop/{ViewModels => }/WorkshopViewModel.cs (95%) diff --git a/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs deleted file mode 100644 index 537b7ec11..000000000 --- a/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputDynamicEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Artemis.Core; - -namespace Artemis.UI.Shared.Events -{ - /// - /// Provides data about selection events raised by - /// - public class DataModelInputDynamicEventArgs : EventArgs - { - internal DataModelInputDynamicEventArgs(DataModelPath? dataModelPath) - { - DataModelPath = dataModelPath; - } - - /// - /// Gets the data model path that was selected - /// - public DataModelPath? DataModelPath { get; } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs deleted file mode 100644 index beba22fae..000000000 --- a/src/Avalonia/Artemis.UI.Shared/Events/DataModelInputStaticEventArgs.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Artemis.UI.Shared.Events -{ - /// - /// Provides data about submit events raised by - /// - public class DataModelInputStaticEventArgs : EventArgs - { - internal DataModelInputStaticEventArgs(object? value) - { - Value = value; - } - - /// - /// The value that was submitted - /// - public object? Value { get; } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs deleted file mode 100644 index 2402b0858..000000000 --- a/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Avalonia; - -namespace Artemis.UI.Shared.Events -{ - public class SelectionRectangleEventArgs : EventArgs - { - public SelectionRectangleEventArgs(Rect rect) - { - Rect = rect; - } - - public Rect Rect { get; } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs index 8d47b2f6d..a9d2f06d3 100644 --- a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs @@ -1,7 +1,7 @@ using Artemis.Core.Ninject; using Artemis.UI.Exceptions; using Artemis.UI.Ninject; -using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Screens.Root; using Artemis.UI.Shared.Ninject; using Avalonia.Controls.ApplicationLifetimes; using Ninject; diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml.cs b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs index c49ecd415..ad4cf0110 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml.cs +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs @@ -1,4 +1,4 @@ -using Artemis.UI.Screens.Root.ViewModels; +using Artemis.UI.Screens.Root; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index b8e08da13..736298c31 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,11 +1,11 @@ using System.Collections.ObjectModel; using Artemis.Core; -using Artemis.UI.Screens.Device.Tabs.ViewModels; -using Artemis.UI.Screens.Device.ViewModels; -using Artemis.UI.Screens.Plugins.ViewModels; -using Artemis.UI.Screens.Root.ViewModels; -using Artemis.UI.Screens.Settings.Tabs.ViewModels; -using Artemis.UI.Screens.SurfaceEditor.ViewModels; +using Artemis.UI.Screens.Device; +using Artemis.UI.Screens.Device.Tabs; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Screens.Root.Sidebar; +using Artemis.UI.Screens.Settings.Tabs; +using Artemis.UI.Screens.SurfaceEditor; using ReactiveUI; namespace Artemis.UI.Ninject.Factories diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputView.axaml similarity index 94% rename from src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputView.axaml index 760e110ec..310120d5c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="1050" - x:Class="Artemis.UI.Screens.Device.Views.DeviceDetectInputView"> + x:Class="Artemis.UI.Screens.Device.DeviceDetectInputView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputView.axaml.cs similarity index 80% rename from src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputView.axaml.cs index c36de345a..e82e7a74f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceDetectInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputView.axaml.cs @@ -1,8 +1,7 @@ -using Artemis.UI.Screens.Device.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Device.Views +namespace Artemis.UI.Screens.Device { public class DeviceDetectInputView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs similarity index 97% rename from src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs index a26178751..e69616629 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceDetectInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceDetectInputViewModel.cs @@ -10,7 +10,7 @@ using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Screens.Device.ViewModels +namespace Artemis.UI.Screens.Device { public class DeviceDetectInputViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml index 5d8fbd58a..3e51dbae6 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" - x:Class="Artemis.UI.Screens.Device.Views.DevicePropertiesView" + x:Class="Artemis.UI.Screens.Device.DevicePropertiesView" Title="Artemis | Device Properties" Width="1250" Height="900" diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs similarity index 83% rename from src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs index 624fc567c..d525d7c7f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Views/DevicePropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs @@ -1,9 +1,8 @@ -using Artemis.UI.Screens.Device.ViewModels; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Device.Views +namespace Artemis.UI.Screens.Device { public partial class DevicePropertiesView : ReactiveWindow { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DevicePropertiesViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs similarity index 96% rename from src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DevicePropertiesViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs index 4ce089f9b..039d776f3 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DevicePropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesViewModel.cs @@ -5,7 +5,7 @@ using Artemis.UI.Shared; using RGB.NET.Core; using ArtemisLed = Artemis.Core.ArtemisLed; -namespace Artemis.UI.Screens.Device.ViewModels +namespace Artemis.UI.Screens.Device { public class DevicePropertiesViewModel : DialogViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsView.axaml similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsView.axaml index d36be0a13..c770b2c3f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Device.Views.DeviceSettingsView"> + x:Class="Artemis.UI.Screens.Device.DeviceSettingsView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsView.axaml.cs similarity index 88% rename from src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsView.axaml.cs index c72252e4a..2f10695a3 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Views/DeviceSettingsView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.Device.Views +namespace Artemis.UI.Screens.Device { public partial class DeviceSettingsView : UserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs similarity index 97% rename from src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceSettingsViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs index db9a9f4f3..c031f8dba 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/ViewModels/DeviceSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.Settings.Tabs.ViewModels; +using Artemis.UI.Screens.Settings.Tabs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Threading; @@ -11,7 +11,7 @@ using Humanizer; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Screens.Device.ViewModels +namespace Artemis.UI.Screens.Device { public class DeviceSettingsViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabView.axaml similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabView.axaml index 3541903cf..5a29d5fa8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Device.Tabs.Views.DeviceInfoTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.DeviceInfoTabView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabView.axaml.cs similarity index 87% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabView.axaml.cs index b08fabd45..6970c9ce7 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceInfoTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs { public partial class DeviceInfoTabView : UserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabViewModel.cs similarity index 95% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabViewModel.cs index c2a96146d..839b930b2 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceInfoTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceInfoTabViewModel.cs @@ -5,7 +5,7 @@ using Artemis.UI.Shared.Services.Interfaces; using Avalonia; using RGB.NET.Core; -namespace Artemis.UI.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs { public class DeviceInfoTabViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml similarity index 83% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml index f3cef6e32..394c08db0 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Device.Tabs.Views.DeviceLedsTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.DeviceLedsTabView"> Welcome to Avalonia! diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml.cs similarity index 87% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml.cs index e0057260e..b2cfe8437 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DeviceLedsTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs { public partial class DeviceLedsTabView : UserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs similarity index 97% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs index 1987a2894..30d48729f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DeviceLedsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs @@ -8,7 +8,7 @@ using Artemis.UI.Shared; using DynamicData.Binding; using ReactiveUI; -namespace Artemis.UI.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs { public class DeviceLedsTabViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml similarity index 97% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml index de3a7059c..a5a5a4585 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml @@ -5,14 +5,14 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" - xmlns:viewModels="clr-namespace:Artemis.UI.Screens.Device.Tabs.ViewModels" + xmlns:tabs="clr-namespace:Artemis.UI.Screens.Device.Tabs" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1200" - x:Class="Artemis.UI.Screens.Device.Tabs.Views.DevicePropertiesTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.DevicePropertiesTabView"> - + diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml.cs similarity index 85% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml.cs index 31e29cbb3..80370bc67 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/DevicePropertiesTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabView.axaml.cs @@ -1,9 +1,8 @@ -using Artemis.UI.Screens.Device.Tabs.ViewModels; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs { public partial class DevicePropertiesTabView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs similarity index 99% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs index a94898182..be267493f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/DevicePropertiesTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -10,7 +10,7 @@ using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; using SkiaSharp; -namespace Artemis.UI.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs { public class DevicePropertiesTabViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabView.axaml similarity index 82% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabView.axaml index 9cb5e59c8..602ca4c5c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Device.Tabs.Views.InputMappingsTabView"> + x:Class="Artemis.UI.Screens.Device.Tabs.InputMappingsTabView"> Welcome to Avalonia! diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabView.axaml.cs similarity index 87% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabView.axaml.cs index 53c7dc6ad..462e1455e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/Views/InputMappingsTabView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabView.axaml.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.Device.Tabs.Views +namespace Artemis.UI.Screens.Device.Tabs { public partial class InputMappingsTabView : UserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs index 3f51b9e70..96cf5d740 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/ViewModels/InputMappingsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs @@ -9,7 +9,7 @@ using Artemis.UI.Shared; using ReactiveUI; using RGB.NET.Core; -namespace Artemis.UI.Screens.Device.Tabs.ViewModels +namespace Artemis.UI.Screens.Device.Tabs { public class InputMappingsTabViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Home/Views/HomeView.axaml b/src/Avalonia/Artemis.UI/Screens/Home/HomeView.axaml similarity index 99% rename from src/Avalonia/Artemis.UI/Screens/Home/Views/HomeView.axaml rename to src/Avalonia/Artemis.UI/Screens/Home/HomeView.axaml index 0bc31a2ea..c34ba7bca 100644 --- a/src/Avalonia/Artemis.UI/Screens/Home/Views/HomeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Home/HomeView.axaml @@ -6,7 +6,7 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="900" - x:Class="Artemis.UI.Screens.Home.Views.HomeView"> + x:Class="Artemis.UI.Screens.Home.HomeView"> { diff --git a/src/Avalonia/Artemis.UI/Screens/Home/ViewModels/HomeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Home/HomeViewModel.cs similarity index 83% rename from src/Avalonia/Artemis.UI/Screens/Home/ViewModels/HomeViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Home/HomeViewModel.cs index f1d087825..1bd0d3032 100644 --- a/src/Avalonia/Artemis.UI/Screens/Home/ViewModels/HomeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Home/HomeViewModel.cs @@ -1,6 +1,6 @@ using ReactiveUI; -namespace Artemis.UI.Screens.Home.ViewModels +namespace Artemis.UI.Screens.Home { public class HomeViewModel : MainScreenViewModel { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs index f4960bb9c..24fd35ba8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs @@ -10,7 +10,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.Dialogs { public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs similarity index 99% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs index 146712b13..0be1f29a8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs @@ -11,7 +11,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins.Dialogs { public class PluginPrerequisitesUninstallDialogViewModel : DialogViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml similarity index 97% rename from src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml index d9d938ae1..7fc48d380 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml @@ -5,7 +5,7 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Plugins.Views.PluginFeatureView"> + x:Class="Artemis.UI.Screens.Plugins.PluginFeatureView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml.cs similarity index 80% rename from src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml.cs index e432b4435..115f00ca0 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginFeatureView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureView.axaml.cs @@ -1,8 +1,7 @@ -using Artemis.UI.Screens.Plugins.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Plugins.Views +namespace Artemis.UI.Screens.Plugins { public partial class PluginFeatureView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs index fdba3ed41..cc7809133 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs @@ -4,12 +4,13 @@ using System.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Screens.Plugins.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using ReactiveUI; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins { public class PluginFeatureViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs similarity index 86% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs index d2ef14784..19b6a3f46 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs @@ -1,7 +1,7 @@ using Artemis.Core; using Artemis.UI.Shared; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins { public class PluginPrerequisiteActionViewModel : ViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs index c7aa6e392..8a2c6c28f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs @@ -7,7 +7,7 @@ using Artemis.Core; using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins { public class PluginPrerequisiteViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml index 3d80c7ab8..8f2796ae8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml @@ -6,7 +6,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Plugins.Views.PluginSettingsView"> + x:Class="Artemis.UI.Screens.Plugins.PluginSettingsView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs similarity index 80% rename from src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs index 31cae533a..5c046aeab 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs @@ -1,8 +1,7 @@ -using Artemis.UI.Screens.Plugins.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Plugins.Views +namespace Artemis.UI.Screens.Plugins { public partial class PluginSettingsView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs similarity index 99% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs index a37561ba7..1702e2e09 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs @@ -9,13 +9,14 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Exceptions; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Plugins.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Threading; using Ninject; using ReactiveUI; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins { public class PluginSettingsViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml similarity index 90% rename from src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 82a8d3393..788b5ff10 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Plugins.Views.PluginSettingsWindowView" + x:Class="Artemis.UI.Screens.Plugins.PluginSettingsWindowView" Title="{Binding DisplayName}" ExtendClientAreaToDecorationsHint="True" Width="800" diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs similarity index 91% rename from src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs index fb152bbf9..67a67d977 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Views/PluginSettingsWindowView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs @@ -1,13 +1,12 @@ using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using Artemis.UI.Screens.Plugins.ViewModels; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using ReactiveUI; -namespace Artemis.UI.Screens.Plugins.Views +namespace Artemis.UI.Screens.Plugins { public class PluginSettingsWindowView : ReactiveWindow { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs similarity index 92% rename from src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs index 78118b2a0..3771d30f0 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/ViewModels/PluginSettingsWindowViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowViewModel.cs @@ -2,7 +2,7 @@ using Artemis.Core; using Artemis.UI.Shared; -namespace Artemis.UI.Screens.Plugins.ViewModels +namespace Artemis.UI.Screens.Plugins { public class PluginSettingsWindowViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml similarity index 82% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 7e808d336..5770a7a23 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -3,6 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.Views.ProfileEditorView"> + x:Class="Artemis.UI.Screens.ProfileEditor.ProfileEditorView"> Welcome to Avalonia! diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml.cs similarity index 77% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml.cs index ca7b4bb7a..6949c7a36 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Views/ProfileEditorView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml.cs @@ -1,8 +1,7 @@ -using Artemis.UI.Screens.ProfileEditor.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.Views +namespace Artemis.UI.Screens.ProfileEditor { public class ProfileEditorView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs similarity index 67% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 5ebeb327d..146bd4ed0 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ViewModels/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -1,6 +1,6 @@ using Artemis.UI.Shared; -namespace Artemis.UI.Screens.ProfileEditor.ViewModels +namespace Artemis.UI.Screens.ProfileEditor { public class ProfileEditorViewModel : ActivatableViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml similarity index 94% rename from src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml rename to src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml index c2bab7563..967bd9260 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Root.Views.RootView"> + x:Class="Artemis.UI.Screens.Root.RootView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml.cs similarity index 79% rename from src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml.cs index 410cd3174..8190bb82e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Views/RootView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml.cs @@ -1,8 +1,7 @@ -using Artemis.UI.Screens.Root.ViewModels; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Root.Views +namespace Artemis.UI.Screens.Root { public class RootView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs similarity index 91% rename from src/Avalonia/Artemis.UI/Screens/Root/ViewModels/RootViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 21b895560..8e6bdda11 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/ViewModels/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -1,10 +1,11 @@ using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Root.Sidebar; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using ReactiveUI; -namespace Artemis.UI.Screens.Root.ViewModels +namespace Artemis.UI.Screens.Root { public class RootViewModel : ActivatableViewModelBase, IScreen { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Views/SidebarCategoryView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml similarity index 98% rename from src/Avalonia/Artemis.UI/Screens/Root/Views/SidebarCategoryView.axaml rename to src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml index 4fe165ef4..500b30c08 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Views/SidebarCategoryView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml @@ -3,9 +3,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:local="clr-namespace:Artemis.UI.Screens.Root.ViewModels" + xmlns:local="clr-namespace:Artemis.UI.Screens.Root.Sidebar" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Root.Views.SidebarCategoryView"> + x:Class="Artemis.UI.Screens.Root.Sidebar.SidebarCategoryView"> + + - - - - - - - - - - - - - + + + HorizontalAlignment="Right" + Command="{Binding EditCategory}"> - - - - - - + + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs index 5c20689eb..c8b1ffbbb 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs @@ -1,22 +1,30 @@ using System.Collections.ObjectModel; using System.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Root.Sidebar.Dialogs; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; +using FluentAvalonia.UI.Controls; using ReactiveUI; namespace Artemis.UI.Screens.Root.Sidebar { public class SidebarCategoryViewModel : ViewModelBase { + private readonly SidebarViewModel _sidebarViewModel; private readonly IProfileService _profileService; + private readonly IWindowService _windowService; private readonly ISidebarVmFactory _vmFactory; private SidebarProfileConfigurationViewModel? _selectedProfileConfiguration; - public SidebarCategoryViewModel(ProfileCategory profileCategory, IProfileService profileService, ISidebarVmFactory vmFactory) + public SidebarCategoryViewModel(SidebarViewModel sidebarViewModel, ProfileCategory profileCategory, IProfileService profileService, IWindowService windowService, ISidebarVmFactory vmFactory) { + _sidebarViewModel = sidebarViewModel; _profileService = profileService; + _windowService = windowService; _vmFactory = vmFactory; ProfileCategory = profileCategory; @@ -62,6 +70,19 @@ namespace Artemis.UI.Screens.Root.Sidebar } } + public async Task EditCategory() + { + await _windowService.CreateContentDialog() + .WithTitle("Edit category") + .WithViewModel(out var vm, ("category", ProfileCategory)) + .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) + .HavingSecondaryButton(b => b.WithText("Delete").WithCommand(vm.Delete)) + .WithDefaultButton(ContentDialogButton.Primary) + .ShowAsync(); + + _sidebarViewModel.UpdateProfileCategories(); + } + private void CreateProfileViewModels() { ProfileConfigurations.Clear(); diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml index b159cb66a..e9cc753c5 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml @@ -48,7 +48,10 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Keybindings + You may set up hotkeys to activate/deactivate the profile + + TODO + + + Activation conditions + If you only want this profile to be active under certain conditions, configure those conditions below + + TODO + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs new file mode 100644 index 000000000..6c822bb91 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Root.Sidebar.Dialogs +{ + public partial class ProfileConfigurationEditView : ReactiveWindow + { + public ProfileConfigurationEditView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs new file mode 100644 index 000000000..4e0261214 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; +using Avalonia.Media.Imaging; +using Avalonia.Svg.Skia; +using Avalonia.Threading; +using Castle.Core.Resource; +using Material.Icons; +using Newtonsoft.Json; +using ReactiveUI; + +namespace Artemis.UI.Screens.Root.Sidebar.Dialogs +{ + public class ProfileConfigurationEditViewModel : DialogViewModelBase + { + private readonly ProfileCategory _profileCategory; + private readonly IProfileService _profileService; + private readonly IWindowService _windowService; + private ProfileConfigurationIconType _iconType; + private ObservableCollection? _materialIcons; + private ProfileConfiguration _profileConfiguration; + private string _profileName; + private Bitmap? _selectedBitmapSource; + private ProfileIconViewModel? _selectedMaterialIcon; + private ProfileModuleViewModel? _selectedModule; + private string? _selectedIconPath; + private SvgImage? _selectedSvgSource; + + public ProfileConfigurationEditViewModel(ProfileCategory profileCategory, ProfileConfiguration? profileConfiguration, IWindowService windowService, + IProfileService profileService, IPluginManagementService pluginManagementService) + { + _profileCategory = profileCategory; + _windowService = windowService; + _profileService = profileService; + _profileConfiguration = profileConfiguration ?? profileService.CreateProfileConfiguration(profileCategory, "New profile", Enum.GetValues().First().ToString()); + _profileName = _profileConfiguration.Name; + _iconType = _profileConfiguration.Icon.IconType; + + IsNew = profileConfiguration == null; + DisplayName = IsNew ? "Artemis | Add profile" : "Artemis | Edit profile"; + Modules = new ObservableCollection( + pluginManagementService.GetFeaturesOfType().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m)) + ); + + Dispatcher.UIThread.Post(LoadIcon, DispatcherPriority.Background); + } + + public bool IsNew { get; } + + public ProfileConfiguration ProfileConfiguration + { + get => _profileConfiguration; + set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value); + } + + public string ProfileName + { + get => _profileName; + set => this.RaiseAndSetIfChanged(ref _profileName, value); + } + + public ObservableCollection Modules { get; } + + public ProfileModuleViewModel? SelectedModule + { + get => _selectedModule; + set => this.RaiseAndSetIfChanged(ref _selectedModule, value); + } + + public async Task Import() + { + string[]? result = await _windowService.CreateOpenFileDialog() + .HavingFilter(f => f.WithExtension("json").WithName("Artemis profile")) + .ShowAsync(); + + if (result == null) + return; + + string json = await File.ReadAllTextAsync(result[0]); + ProfileConfigurationExportModel? profileConfigurationExportModel = null; + try + { + profileConfigurationExportModel = JsonConvert.DeserializeObject(json, IProfileService.ExportSettings); + } + catch (JsonException e) + { + _windowService.ShowExceptionDialog("Import profile failed", e); + } + + if (profileConfigurationExportModel == null) + { + await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null); + return; + } + + _profileService.ImportProfile(_profileCategory, profileConfigurationExportModel); + Close(true); + } + + public async Task Confirm() + { + ProfileConfiguration.Name = ProfileName; + ProfileConfiguration.Module = SelectedModule?.Module; + await SaveIcon(); + + _profileService.SaveProfileConfigurationIcon(ProfileConfiguration); + _profileService.SaveProfileCategory(_profileCategory); + Close(true); + } + + public void Cancel() + { + if (IsNew) + _profileService.RemoveProfileConfiguration(_profileConfiguration); + Close(false); + } + + #region Icon + + public ProfileConfigurationIconType IconType + { + get => _iconType; + set => this.RaiseAndSetIfChanged(ref _iconType, value); + } + + public ObservableCollection? MaterialIcons + { + get => _materialIcons; + set => this.RaiseAndSetIfChanged(ref _materialIcons, value); + } + + public ProfileIconViewModel? SelectedMaterialIcon + { + get => _selectedMaterialIcon; + set => this.RaiseAndSetIfChanged(ref _selectedMaterialIcon, value); + } + + public Bitmap? SelectedBitmapSource + { + get => _selectedBitmapSource; + set => this.RaiseAndSetIfChanged(ref _selectedBitmapSource, value); + } + + public SvgImage? SelectedSvgSource + { + get => _selectedSvgSource; + set => this.RaiseAndSetIfChanged(ref _selectedSvgSource, value); + } + + private void LoadIcon() + { + // Preselect the icon based on streams if needed + if (_profileConfiguration.Icon.IconType == ProfileConfigurationIconType.BitmapImage) + { + SelectedBitmapSource = new Bitmap(_profileConfiguration.Icon.GetIconStream()); + } + else if (_profileConfiguration.Icon.IconType == ProfileConfigurationIconType.SvgImage) + { + SvgSource newSource = new(); + newSource.Load(_profileConfiguration.Icon.GetIconStream()); + SelectedSvgSource = new SvgImage {Source = newSource}; + } + + // Prepare the contents of the dropdown box, it should be virtualized so no need to wait with this + MaterialIcons = new ObservableCollection(Enum.GetValues() + .Select(kind => new ProfileIconViewModel(kind)) + .DistinctBy(vm => vm.DisplayName) + .OrderBy(vm => vm.DisplayName)); + + // Preselect the icon or fall back to a random one + SelectedMaterialIcon = !IsNew && Enum.TryParse(_profileConfiguration.Icon.IconName, out MaterialIconKind enumValue) + ? MaterialIcons.FirstOrDefault(m => m.Icon == enumValue) + : MaterialIcons.ElementAt(new Random().Next(0, MaterialIcons.Count - 1)); + } + + private async Task SaveIcon() + { + if (IconType == ProfileConfigurationIconType.MaterialIcon && SelectedMaterialIcon != null) + ProfileConfiguration.Icon.SetIconByName(SelectedMaterialIcon.Icon.ToString()); + else if (_selectedIconPath != null) + { + await using FileStream fileStream = File.OpenRead(_selectedIconPath); + ProfileConfiguration.Icon.SetIconByStream(Path.GetFileName(_selectedIconPath), fileStream); + } + } + + public async Task BrowseBitmapFile() + { + string[]? result = await _windowService.CreateOpenFileDialog() + .HavingFilter(f => f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image")) + .ShowAsync(); + + if (result == null) + return; + + SelectedBitmapSource = new Bitmap(result[0]); + _selectedIconPath = result[0]; + } + + public async Task BrowseSvgFile() + { + string[]? result = await _windowService.CreateOpenFileDialog() + .HavingFilter(f => f.WithExtension("svg").WithName("SVG image")) + .ShowAsync(); + + if (result == null) + return; + + SvgSource newSource = new(); + newSource.Load(result[0]); + + SelectedSvgSource = new SvgImage {Source = newSource}; + _selectedIconPath = result[0]; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileIconViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileIconViewModel.cs new file mode 100644 index 000000000..11417df61 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileIconViewModel.cs @@ -0,0 +1,16 @@ +using Artemis.UI.Shared; +using Material.Icons; + +namespace Artemis.UI.Screens.Root.Sidebar.Dialogs +{ + public class ProfileIconViewModel : ViewModelBase + { + public ProfileIconViewModel(MaterialIconKind icon) + { + Icon = icon; + DisplayName = icon.ToString(); + } + + public MaterialIconKind Icon { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileModuleViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileModuleViewModel.cs new file mode 100644 index 000000000..4452babb6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileModuleViewModel.cs @@ -0,0 +1,23 @@ +using Artemis.Core.Modules; +using Artemis.UI.Shared; +using Material.Icons; + +namespace Artemis.UI.Screens.Root.Sidebar.Dialogs +{ + public class ProfileModuleViewModel : ViewModelBase + { + public ProfileModuleViewModel(Module module) + { + Module = module; + Name = module.Info.Name; + Icon = module.Info.ResolvedIcon ?? MaterialIconKind.QuestionMark.ToString(); + Description = module.Info.Description; + } + + public string Icon { get; } + public string Name { get; } + public string? Description { get; } + + public Module Module { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml index da722c486..7c4ea26be 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml @@ -29,7 +29,7 @@ - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs index c8b1ffbbb..3093c95cf 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs @@ -74,15 +74,21 @@ namespace Artemis.UI.Screens.Root.Sidebar { await _windowService.CreateContentDialog() .WithTitle("Edit category") - .WithViewModel(out var vm, ("category", ProfileCategory)) + .WithViewModel(out var vm, ("category", ProfileCategory)) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) .HavingSecondaryButton(b => b.WithText("Delete").WithCommand(vm.Delete)) + .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) .ShowAsync(); _sidebarViewModel.UpdateProfileCategories(); } + public async Task AddProfile() + { + await _windowService.ShowDialogAsync(("profileCategory", ProfileCategory), ("profileConfiguration", null)); + } + private void CreateProfileViewModels() { ProfileConfigurations.Clear(); diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml index e9cc753c5..e98beae43 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml @@ -7,22 +7,8 @@ xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Root.Sidebar.SidebarView"> - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs index fa14d3652..8cabe8409 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs @@ -90,8 +90,9 @@ namespace Artemis.UI.Screens.Root.Sidebar { await _windowService.CreateContentDialog() .WithTitle("Add new category") - .WithViewModel(out var vm, ("category", null)) + .WithViewModel(out var vm, ("category", null)) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) + .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) .ShowAsync(); diff --git a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml index c40b34e96..7c51cbf0f 100644 --- a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml @@ -6,7 +6,6 @@ - Yellow From 519f6bb44d3415f5be48e2ceb211bea73e678217 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 5 Dec 2021 00:20:24 +0100 Subject: [PATCH 089/270] UI - Implemented profile create/update/delete --- .../ProfileConfigurationIcon.cs | 51 +++++++------- .../ProfileConfigurationIcon.axaml.cs | 66 +++++++++---------- .../Ninject/Factories/IVMFactory.cs | 2 +- .../ProfileConfigurationEditView.axaml | 15 ++--- .../ProfileConfigurationEditViewModel.cs | 25 ++++++- .../Root/Sidebar/SidebarCategoryViewModel.cs | 6 +- .../SidebarProfileConfigurationView.axaml | 3 +- .../SidebarProfileConfigurationViewModel.cs | 17 ++++- 8 files changed, 112 insertions(+), 73 deletions(-) diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs index ff205f93d..46e5595a5 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -8,13 +8,10 @@ namespace Artemis.Core /// /// Represents the icon of a /// - public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel + public class ProfileConfigurationIcon : IStorageModel { private readonly ProfileConfigurationEntity _entity; - private string? _iconName; private Stream? _iconStream; - private ProfileConfigurationIconType _iconType; - private string? _originalFileName; internal ProfileConfigurationIcon(ProfileConfigurationEntity entity) { @@ -24,29 +21,17 @@ namespace Artemis.Core /// /// Gets the type of icon this profile configuration uses /// - public ProfileConfigurationIconType IconType - { - get => _iconType; - private set => SetAndNotify(ref _iconType, value); - } + public ProfileConfigurationIconType IconType { get; private set; } /// /// Gets the name of the icon if is /// - public string? IconName - { - get => _iconName; - private set => SetAndNotify(ref _iconName, value); - } + public string? IconName { get; private set; } /// /// Gets the original file name of the icon (if applicable) /// - public string? OriginalFileName - { - get => _originalFileName; - private set => SetAndNotify(ref _originalFileName, value); - } + public string? OriginalFileName { get; private set; } /// /// Updates the to the provided value and changes the is @@ -55,11 +40,14 @@ namespace Artemis.Core /// The name of the icon public void SetIconByName(string iconName) { - IconName = iconName ?? throw new ArgumentNullException(nameof(iconName)); + if (iconName == null) throw new ArgumentNullException(nameof(iconName)); + + _iconStream?.Dispose(); + IconName = iconName; OriginalFileName = null; IconType = ProfileConfigurationIconType.MaterialIcon; - _iconStream?.Dispose(); + OnIconUpdated(); } /// @@ -81,6 +69,7 @@ namespace Artemis.Core IconName = null; OriginalFileName = originalFileName; IconType = OriginalFileName.EndsWith(".svg") ? ProfileConfigurationIconType.SvgImage : ProfileConfigurationIconType.BitmapImage; + OnIconUpdated(); } /// @@ -100,14 +89,30 @@ namespace Artemis.Core return stream; } + /// + /// Occurs when the icon was updated + /// + public event EventHandler? IconUpdated; + + /// + /// Invokes the event + /// + protected virtual void OnIconUpdated() + { + IconUpdated?.Invoke(this, EventArgs.Empty); + } + #region Implementation of IStorageModel /// public void Load() { IconType = (ProfileConfigurationIconType) _entity.IconType; - if (IconType == ProfileConfigurationIconType.MaterialIcon) - IconName = _entity.MaterialIcon; + if (IconType != ProfileConfigurationIconType.MaterialIcon) + return; + + IconName = _entity.MaterialIcon; + OnIconUpdated(); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs index 6cbd39a0a..3037af3a8 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel; using System.IO; using Artemis.Core; using Avalonia; @@ -15,25 +14,6 @@ namespace Artemis.UI.Shared.Controls { public class ProfileConfigurationIcon : UserControl { - #region Properties - - /// - /// Gets or sets the to display - /// - public static readonly StyledProperty ConfigurationIconProperty = - AvaloniaProperty.Register(nameof(ConfigurationIcon)); - - /// - /// Gets or sets the to display - /// - public Core.ProfileConfigurationIcon? ConfigurationIcon - { - get => GetValue(ConfigurationIconProperty); - set => SetValue(ConfigurationIconProperty, value); - } - - #endregion - public ProfileConfigurationIcon() { InitializeComponent(); @@ -71,9 +51,13 @@ namespace Artemis.UI.Shared.Controls Content = new Image {Source = new SvgImage {Source = source}}; } else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.BitmapImage) + { Content = new Image {Source = new Bitmap(ConfigurationIcon.GetIconStream())}; + } else + { Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; + } } catch { @@ -86,34 +70,50 @@ namespace Artemis.UI.Shared.Controls AvaloniaXamlLoader.Load(this); } - #region Event handlers - private void OnDetachedFromLogicalTree(object? sender, LogicalTreeAttachmentEventArgs e) { if (ConfigurationIcon != null) - ConfigurationIcon.PropertyChanged -= IconOnPropertyChanged; + ConfigurationIcon.IconUpdated -= ConfigurationIconOnIconUpdated; - if (Content is Image image && image.Source is IDisposable disposable) + if (Content is Image image && image.Source is IDisposable disposable) disposable.Dispose(); } private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { - if (e.Property == ConfigurationIconProperty) - { - if (e.OldValue is Core.ProfileConfigurationIcon oldIcon) - oldIcon.PropertyChanged -= IconOnPropertyChanged; - if (e.NewValue is Core.ProfileConfigurationIcon newIcon) - newIcon.PropertyChanged += IconOnPropertyChanged; - Update(); - } + if (e.Property != ConfigurationIconProperty) + return; + + if (e.OldValue is Core.ProfileConfigurationIcon oldIcon) + oldIcon.IconUpdated -= ConfigurationIconOnIconUpdated; + if (e.NewValue is Core.ProfileConfigurationIcon newIcon) + newIcon.IconUpdated += ConfigurationIconOnIconUpdated; + + Update(); } - private void IconOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + private void ConfigurationIconOnIconUpdated(object? sender, EventArgs e) { Update(); } + #region Properties + + /// + /// Gets or sets the to display + /// + public static readonly StyledProperty ConfigurationIconProperty = + AvaloniaProperty.Register(nameof(ConfigurationIcon)); + + /// + /// Gets or sets the to display + /// + public Core.ProfileConfigurationIcon? ConfigurationIcon + { + get => GetValue(ConfigurationIconProperty); + set => SetValue(ConfigurationIconProperty, value); + } + #endregion } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 7f83f4027..2248b2bdc 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -36,7 +36,7 @@ namespace Artemis.UI.Ninject.Factories { SidebarViewModel? SidebarViewModel(IScreen hostScreen); SidebarCategoryViewModel SidebarCategoryViewModel(SidebarViewModel sidebarViewModel, ProfileCategory profileCategory); - SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); + SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration); } public interface SurfaceVmFactory : IVmFactory diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml index 8b36f089b..88bb927b5 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml @@ -72,10 +72,10 @@ - + - + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index 4e0261214..b152900fc 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -76,6 +76,9 @@ namespace Artemis.UI.Screens.Root.Sidebar.Dialogs public async Task Import() { + if (!IsNew) + return; + string[]? result = await _windowService.CreateOpenFileDialog() .HavingFilter(f => f.WithExtension("json").WithName("Artemis profile")) .ShowAsync(); @@ -100,7 +103,22 @@ namespace Artemis.UI.Screens.Root.Sidebar.Dialogs return; } + // Remove the temporary profile configuration + _profileService.RemoveProfileConfiguration(_profileConfiguration); + // Import the new profile configuration _profileService.ImportProfile(_profileCategory, profileConfigurationExportModel); + + Close(true); + } + + public async Task Delete() + { + if (IsNew) + return; + if (!await _windowService.ShowConfirmContentDialog("Delete profile", "Are you sure you want to permanently delete this profile?")) + return; + + _profileService.RemoveProfileConfiguration(_profileConfiguration); Close(true); } @@ -169,15 +187,16 @@ namespace Artemis.UI.Screens.Root.Sidebar.Dialogs } // Prepare the contents of the dropdown box, it should be virtualized so no need to wait with this - MaterialIcons = new ObservableCollection(Enum.GetValues() + ObservableCollection icons = new(Enum.GetValues() .Select(kind => new ProfileIconViewModel(kind)) .DistinctBy(vm => vm.DisplayName) .OrderBy(vm => vm.DisplayName)); // Preselect the icon or fall back to a random one SelectedMaterialIcon = !IsNew && Enum.TryParse(_profileConfiguration.Icon.IconName, out MaterialIconKind enumValue) - ? MaterialIcons.FirstOrDefault(m => m.Icon == enumValue) - : MaterialIcons.ElementAt(new Random().Next(0, MaterialIcons.Count - 1)); + ? icons.FirstOrDefault(m => m.Icon == enumValue) + : icons.ElementAt(new Random().Next(0, icons.Count - 1)); + MaterialIcons = icons; } private async Task SaveIcon() diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs index 3093c95cf..ac865ee3e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs @@ -86,14 +86,16 @@ namespace Artemis.UI.Screens.Root.Sidebar public async Task AddProfile() { - await _windowService.ShowDialogAsync(("profileCategory", ProfileCategory), ("profileConfiguration", null)); + bool result = await _windowService.ShowDialogAsync(("profileCategory", ProfileCategory), ("profileConfiguration", null)); + if (result) + _sidebarViewModel.UpdateProfileCategories(); } private void CreateProfileViewModels() { ProfileConfigurations.Clear(); foreach (ProfileConfiguration profileConfiguration in ProfileCategory.ProfileConfigurations.OrderBy(p => p.Order)) - ProfileConfigurations.Add(_vmFactory.SidebarProfileConfigurationViewModel(profileConfiguration)); + ProfileConfigurations.Add(_vmFactory.SidebarProfileConfigurationViewModel(_sidebarViewModel, profileConfiguration)); SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(i => i.ProfileConfiguration.IsBeingEdited); } diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarProfileConfigurationView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarProfileConfigurationView.axaml index 093d3aced..01627b418 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarProfileConfigurationView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarProfileConfigurationView.axaml @@ -106,7 +106,8 @@ - - - + + From b801de1f302d3cd9df3ca9d0a3584921920053bd Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 6 Dec 2021 00:44:25 +0100 Subject: [PATCH 091/270] Windows - Added input provider Debugger - Fixed data model list display --- src/Artemis.Core/Constants.cs | 2 +- .../Services/Input/InputService.cs | 6 +- .../Artemis.UI.Avalonia.Shared.xml | 60 +- ...Model.cs => DataModelListItemViewModel.cs} | 28 +- .../DataModelListPropertiesViewModel.cs | 83 - .../Shared/DataModelListViewModel.cs | 23 +- .../Shared/DataModelVisualizationViewModel.cs | 2 +- .../Display/DefaultDataModelDisplayView.axaml | 13 +- .../DefaultDataModelDisplayViewModel.cs | 36 +- .../Artemis.UI.Shared/Styles/Sidebar.axaml | 4 +- src/Avalonia/Artemis.UI.Windows/App.axaml.cs | 22 +- .../Artemis.UI.Windows.csproj | 3 + .../Providers/Input/SpongeWindow.cs | 26 + .../Providers/Input/SpongeWindowEventArgs.cs | 20 + .../Providers/Input/WindowsInputProvider.cs | 281 ++++ .../Utilities/InputUtilities.cs | 1400 +++++++++++++++++ .../Utilities/WindowUtilities.cs | 23 + .../Artemis.UI.Windows/packages.lock.json | 38 +- .../Providers/AvaloniaInputProvider.cs | 8 - .../Screens/Debugger/DebugView.axaml.cs | 5 + .../Tabs/DataModel/DataModelDebugView.axaml | 79 +- .../Performance/PerformanceDebugView.axaml | 35 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 25 +- .../Interfaces/IRegistrationService.cs | 1 - .../Services/RegistrationService.cs | 6 - 25 files changed, 1941 insertions(+), 288 deletions(-) rename src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/{DataModelListPropertyViewModel.cs => DataModelListItemViewModel.cs} (64%) delete mode 100644 src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindow.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindowEventArgs.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Utilities/InputUtilities.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Utilities/WindowUtilities.cs delete mode 100644 src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 1de1f02e4..b63d493c2 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -27,7 +27,7 @@ namespace Artemis.Core /// /// The base path for Artemis application data folder /// - public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows() ? Environment.SpecialFolder.ApplicationData : Environment.SpecialFolder.LocalApplicationData); + public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows() ? Environment.SpecialFolder.CommonApplicationData : Environment.SpecialFolder.LocalApplicationData); /// /// The full path to the Artemis data folder diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index 9b42bc54f..510b095f7 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -25,7 +25,11 @@ namespace Artemis.Core.Services public void Dispose() { while (_inputProviders.Any()) - RemoveInputProvider(_inputProviders.First()); + { + InputProvider provider = _inputProviders.First(); + RemoveInputProvider(provider); + provider.Dispose(); + } } #endregion diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 44de98edf..36b985041 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -374,67 +374,39 @@ - - - Represents a view model that wraps a regular but contained in - a - - - - - Gets the index of the element within the list - - - - - Gets the type of elements contained in the list - - - - - Gets the value of the property that is being visualized - - - - - Gets the view model that handles displaying the property - - - - - - - - - - - - - - - + Represents a view model that visualizes a single data model property contained in a - + + + Gets the view model used to display the display value + + + Gets the index of the element within the list - + Gets the type of elements contained in the list - + + + Gets the value of the property that is being visualized + + + - + - + diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs similarity index 64% rename from src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs rename to src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs index 161b76cbe..1697e164f 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs @@ -9,22 +9,33 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared /// Represents a view model that visualizes a single data model property contained in a /// /// - public class DataModelListPropertyViewModel : DataModelPropertyViewModel + public class DataModelListItemViewModel : DataModelVisualizationViewModel { private int _index; private Type? _listType; + private object? _displayValue; + private DataModelDisplayViewModel? _displayViewModel; - internal DataModelListPropertyViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null) + internal DataModelListItemViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null) { ListType = listType; DisplayViewModel = displayViewModel; } - internal DataModelListPropertyViewModel(Type listType, string? name) : base(null, null, null) + internal DataModelListItemViewModel(Type listType, string? name) : base(null, null, null) { ListType = listType; } + /// + /// Gets the view model used to display the display value + /// + public DataModelDisplayViewModel? DisplayViewModel + { + get => _displayViewModel; + internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value); + } + /// /// Gets the index of the element within the list /// @@ -43,6 +54,15 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared private set => this.RaiseAndSetIfChanged(ref _listType, value); } + /// + /// Gets the value of the property that is being visualized + /// + public object? DisplayValue + { + get => _displayValue; + internal set => this.RaiseAndSetIfChanged(ref _displayValue, value); + } + /// public override object? GetCurrentValue() { @@ -66,7 +86,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared /// public override string ToString() { - return $"[List item {Index}] {DisplayPath ?? Path} - {DisplayValue}"; + return $"[List item {Index}]"; } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs deleted file mode 100644 index 8bd21ea38..000000000 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Linq; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Interfaces; -using ReactiveUI; - -namespace Artemis.UI.Shared.DataModelVisualization.Shared -{ - /// - /// Represents a view model that wraps a regular but contained in - /// a - /// - public class DataModelListPropertiesViewModel : DataModelPropertiesViewModel - { - private object? _displayValue; - private int _index; - private Type? _listType; - - internal DataModelListPropertiesViewModel(Type listType, string? name) : base(null, null, null) - { - ListType = listType; - } - - /// - /// Gets the index of the element within the list - /// - public int Index - { - get => _index; - set => this.RaiseAndSetIfChanged(ref _index, value); - } - - /// - /// Gets the type of elements contained in the list - /// - public Type? ListType - { - get => _listType; - set => this.RaiseAndSetIfChanged(ref _listType, value); - } - - /// - /// Gets the value of the property that is being visualized - /// - public new object? DisplayValue - { - get => _displayValue; - set => this.RaiseAndSetIfChanged(ref _displayValue, value); - } - - /// - /// Gets the view model that handles displaying the property - /// - public DataModelVisualizationViewModel? DisplayViewModel => Children.FirstOrDefault(); - - /// - public override string? DisplayPath => null; - - /// - public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration) - { - PopulateProperties(dataModelUIService, configuration); - if (DisplayViewModel == null) - return; - - if (IsVisualizationExpanded && !DisplayViewModel.IsVisualizationExpanded) - DisplayViewModel.IsVisualizationExpanded = IsVisualizationExpanded; - DisplayViewModel.Update(dataModelUIService, null); - } - - /// - public override object? GetCurrentValue() - { - return DisplayValue; - } - - /// - public override string ToString() - { - return $"[List item {Index}] {DisplayPath ?? Path}"; - } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs index 975a06295..9ec4838d9 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs @@ -98,22 +98,15 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared ListChildren.Add(child); } else - { child = ListChildren[index]; - } - if (child is DataModelListPropertiesViewModel dataModelListClassViewModel) - { - dataModelListClassViewModel.DisplayValue = item; - dataModelListClassViewModel.Index = index; - } - else if (child is DataModelListPropertyViewModel dataModelListPropertyViewModel) + if (child is DataModelListItemViewModel dataModelListPropertyViewModel) { dataModelListPropertyViewModel.DisplayValue = item; dataModelListPropertyViewModel.Index = index; + dataModelListPropertyViewModel.Update(dataModelUIService, configuration); } - child.Update(dataModelUIService, configuration); index++; } @@ -135,16 +128,10 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared { // If a display VM was found, prefer to use that in any case DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(listType, PropertyDescription); - if (typeViewModel != null) - return new DataModelListPropertyViewModel(listType, typeViewModel, name); - // For primitives, create a property view model, it may be null that is fine - if (listType.IsPrimitive || listType.IsEnum || listType == typeof(string)) - return new DataModelListPropertyViewModel(listType, name); - // For other value types create a child view model - if (listType.IsClass || listType.IsStruct()) - return new DataModelListPropertiesViewModel(listType, name); - return null; + return typeViewModel != null + ? new DataModelListItemViewModel(listType, typeViewModel, name) + : new DataModelListItemViewModel(listType, name); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index aaa651957..aef7040af 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -215,7 +215,7 @@ namespace Artemis.UI.Shared.DataModelVisualization.Shared foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) { string childPath = AppendToPath(propertyInfo.Name); - if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) + if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath))) continue; if (propertyInfo.GetCustomAttribute() != null) continue; diff --git a/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml b/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml index 5a9933b5f..8b3e3f3ee 100644 --- a/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml +++ b/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml @@ -13,17 +13,8 @@ Margin="0 0 5 0" /> - - - + + internal class DefaultDataModelDisplayViewModel : DataModelDisplayViewModel { - private bool _showNull; - private bool _showToString; + private readonly JsonSerializerSettings _serializerSettings; + private string _display; - internal DefaultDataModelDisplayViewModel() + public DefaultDataModelDisplayViewModel() { - ShowNull = true; + _serializerSettings = new JsonSerializerSettings() + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + PreserveReferencesHandling = PreserveReferencesHandling.None + }; } - public bool ShowToString + public string Display { - get => _showToString; - private set => this.RaiseAndSetIfChanged(ref _showToString, value); + get => _display; + set => this.RaiseAndSetIfChanged(ref _display, value); } - - public bool ShowNull - { - get => _showNull; - set => this.RaiseAndSetIfChanged(ref _showNull, value); - } - + protected override void OnDisplayValueUpdated() { if (DisplayValue is Enum enumDisplayValue) - DisplayValue = EnumUtilities.HumanizeValue(enumDisplayValue); - - ShowToString = DisplayValue != null; - ShowNull = DisplayValue == null; + Display = EnumUtilities.HumanizeValue(enumDisplayValue); + else if (DisplayValue is not string) + Display = JsonConvert.SerializeObject(DisplayValue, _serializerSettings); + else + Display = DisplayValue?.ToString() ?? "null"; } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Sidebar.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Sidebar.axaml index e4d4ac6b1..95359910d 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Sidebar.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Sidebar.axaml @@ -13,7 +13,9 @@ + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs index 4d6c655bf..e37a0763f 100644 --- a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs @@ -1,3 +1,6 @@ +using Artemis.Core.Services; +using Artemis.UI.Windows.Providers; +using Artemis.UI.Windows.Providers.Input; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; @@ -9,16 +12,13 @@ namespace Artemis.UI.Windows { public class App : Application { - // ReSharper disable NotAccessedField.Local - private StandardKernel? _kernel; - private ApplicationStateManager? _applicationStateManager; - // ReSharper restore NotAccessedField.Local - public override void Initialize() { _kernel = ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); + + RegisterProviders(_kernel); } public override void OnFrameworkInitializationCompleted() @@ -27,5 +27,17 @@ namespace Artemis.UI.Windows if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); } + + private void RegisterProviders(StandardKernel standardKernel) + { + IInputService inputService = standardKernel.Get(); + inputService.AddInputProvider(standardKernel.Get()); + } + + // ReSharper disable NotAccessedField.Local + private StandardKernel? _kernel; + + private ApplicationStateManager? _applicationStateManager; + // ReSharper restore NotAccessedField.Local } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 36ed75d6c..3e6be8758 100644 --- a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -14,6 +14,9 @@ + + + diff --git a/src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindow.cs b/src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindow.cs new file mode 100644 index 000000000..c2b1ff85c --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindow.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Win32; + +namespace Artemis.UI.Windows.Providers.Input +{ + public class SpongeWindow : WindowImpl + { + #region Overrides of WindowImpl + + /// + protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + OnWndProcCalled(new SpongeWindowEventArgs(hWnd, msg, wParam, lParam)); + return base.WndProc(hWnd, msg, wParam, lParam); + } + + #endregion + + public event EventHandler? WndProcCalled; + + protected virtual void OnWndProcCalled(SpongeWindowEventArgs e) + { + WndProcCalled?.Invoke(this, e); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindowEventArgs.cs b/src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindowEventArgs.cs new file mode 100644 index 000000000..bdf5ec87e --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Providers/Input/SpongeWindowEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.UI.Windows.Providers.Input +{ + public class SpongeWindowEventArgs : EventArgs + { + public IntPtr HWnd { get; } + public uint Msg { get; } + public IntPtr WParam { get; } + public IntPtr LParam { get; } + + public SpongeWindowEventArgs(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + HWnd = hWnd; + Msg = msg; + WParam = wParam; + LParam = lParam; + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs b/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs new file mode 100644 index 000000000..4aca8e831 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs @@ -0,0 +1,281 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Timers; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Windows.Utilities; +using Linearstar.Windows.RawInput; +using Linearstar.Windows.RawInput.Native; +using Serilog; +using MouseButton = Artemis.Core.Services.MouseButton; + +namespace Artemis.UI.Windows.Providers.Input +{ + public class WindowsInputProvider : InputProvider + { + private const int WM_INPUT = 0x00FF; + + private readonly IInputService _inputService; + private readonly ILogger _logger; + private DateTime _lastMouseUpdate; + private int _lastProcessId; + private SpongeWindow _sponge; + private readonly Timer _taskManagerTimer; + + public WindowsInputProvider(ILogger logger, IInputService inputService) + { + _logger = logger; + _inputService = inputService; + + _sponge = new SpongeWindow(); + _sponge.WndProcCalled += SpongeOnWndProcCalled; + + _taskManagerTimer = new Timer(500); + _taskManagerTimer.Elapsed += TaskManagerTimerOnElapsed; + _taskManagerTimer.Start(); + + RawInputDevice.RegisterDevice(HidUsageAndPage.Keyboard, RawInputDeviceFlags.InputSink, _sponge.Handle.Handle); + RawInputDevice.RegisterDevice(HidUsageAndPage.Mouse, RawInputDeviceFlags.InputSink, _sponge.Handle.Handle); + } + + + #region Overrides of InputProvider + + /// + public override void OnKeyboardToggleStatusRequested() + { + UpdateToggleStatus(); + } + + #endregion + + #region IDisposable + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _sponge.Dispose(); + _taskManagerTimer.Dispose(); + } + + base.Dispose(disposing); + } + + #endregion + + private void SpongeOnWndProcCalled(object? sender, SpongeWindowEventArgs message) + { + if (message.Msg != WM_INPUT) + return; + + RawInputData data = RawInputData.FromHandle(message.LParam); + switch (data) + { + case RawInputMouseData mouse: + HandleMouseData(data, mouse); + break; + case RawInputKeyboardData keyboard: + HandleKeyboardData(data, keyboard); + break; + } + } + + private void TaskManagerTimerOnElapsed(object sender, ElapsedEventArgs e) + { + int processId = WindowUtilities.GetActiveProcessId(); + if (processId == _lastProcessId) + return; + + _lastProcessId = processId; + + // If task manager has focus then we can't track keys properly, release everything to avoid them getting stuck + // Same goes for Idle which is what you get when you press Ctrl+Alt+Del + Process active = Process.GetProcessById(processId); + if (active?.ProcessName == "Taskmgr" || active?.ProcessName == "Idle") + _inputService.ReleaseAll(); + } + + #region Keyboard + + private void HandleKeyboardData(RawInputData data, RawInputKeyboardData keyboardData) + { + KeyboardKey key = InputUtilities.KeyFromVirtualKey(keyboardData.Keyboard.VirutalKey); + // Debug.WriteLine($"VK: {key} ({keyboardData.Keyboard.VirutalKey}), Flags: {keyboardData.Keyboard.Flags}, Scan code: {keyboardData.Keyboard.ScanCode}"); + + // Sometimes we get double hits and they resolve to None, ignore those + if (key == KeyboardKey.None) + return; + + // Right alt triggers LeftCtrl with a different scan code for some reason, ignore those + if (key == KeyboardKey.LeftCtrl && keyboardData.Keyboard.ScanCode == 56) + return; + + string identifier = data.Device?.DevicePath; + + // Let the core know there is an identifier so it can store new identifications if applicable + if (identifier != null) + OnIdentifierReceived(identifier, InputDeviceType.Keyboard); + + ArtemisDevice device = null; + if (identifier != null) + try + { + device = _inputService.GetDeviceByIdentifier(this, identifier, InputDeviceType.Keyboard); + } + catch (Exception e) + { + _logger.Warning(e, "Failed to retrieve input device by its identifier"); + } + + // Duplicate keys with different positions can be identified by the LeftKey flag (even though its set of the key that's physically on the right) + if (keyboardData.Keyboard.Flags == RawKeyboardFlags.KeyE0 || keyboardData.Keyboard.Flags == (RawKeyboardFlags.KeyE0 | RawKeyboardFlags.Up)) + { + if (key == KeyboardKey.Enter) + key = KeyboardKey.NumPadEnter; + if (key == KeyboardKey.LeftCtrl) + key = KeyboardKey.RightCtrl; + if (key == KeyboardKey.LeftAlt) + key = KeyboardKey.RightAlt; + } + + if (key == KeyboardKey.LeftShift && keyboardData.Keyboard.ScanCode == 54) + key = KeyboardKey.RightShift; + + bool isDown = keyboardData.Keyboard.Flags != RawKeyboardFlags.Up && + keyboardData.Keyboard.Flags != (RawKeyboardFlags.Up | RawKeyboardFlags.KeyE0) && + keyboardData.Keyboard.Flags != (RawKeyboardFlags.Up | RawKeyboardFlags.KeyE1); + + OnKeyboardDataReceived(device, key, isDown); + UpdateToggleStatus(); + } + + private void UpdateToggleStatus() + { + OnKeyboardToggleStatusReceived(new KeyboardToggleStatus( + InputUtilities.IsKeyToggled(KeyboardKey.NumLock), + InputUtilities.IsKeyToggled(KeyboardKey.CapsLock), + InputUtilities.IsKeyToggled(KeyboardKey.Scroll) + )); + } + + #endregion + + #region Mouse + + private int _mouseDeltaX; + private int _mouseDeltaY; + + private void HandleMouseData(RawInputData data, RawInputMouseData mouseData) + { + // Only submit mouse movement 25 times per second but increment the delta + // This can create a small inaccuracy of course, but Artemis is not a shooter :') + if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None) + { + _mouseDeltaX += mouseData.Mouse.LastX; + _mouseDeltaY += mouseData.Mouse.LastY; + if (DateTime.Now - _lastMouseUpdate < TimeSpan.FromMilliseconds(40)) + return; + } + + ArtemisDevice device = null; + string identifier = data.Device?.DevicePath; + if (identifier != null) + try + { + device = _inputService.GetDeviceByIdentifier(this, identifier, InputDeviceType.Keyboard); + } + catch (Exception e) + { + _logger.Warning(e, "Failed to retrieve input device by its identifier"); + } + + // Debug.WriteLine($"Buttons: {mouseData.Mouse.Buttons}, Data: {mouseData.Mouse.ButtonData}, Flags: {mouseData.Mouse.Flags}, XY: {mouseData.Mouse.LastX},{mouseData.Mouse.LastY}"); + + // Movement + if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None) + { + Win32Point cursorPosition = GetCursorPosition(); + OnMouseMoveDataReceived(device, cursorPosition.X, cursorPosition.Y, _mouseDeltaX, _mouseDeltaY); + _mouseDeltaX = 0; + _mouseDeltaY = 0; + _lastMouseUpdate = DateTime.Now; + return; + } + + // Now we know its not movement, let the core know there is an identifier so it can store new identifications if applicable + if (identifier != null) + OnIdentifierReceived(identifier, InputDeviceType.Mouse); + + // Scrolling + if (mouseData.Mouse.ButtonData != 0) + { + if (mouseData.Mouse.Buttons == RawMouseButtonFlags.MouseWheel) + OnMouseScrollDataReceived(device, MouseScrollDirection.Vertical, mouseData.Mouse.ButtonData); + else if (mouseData.Mouse.Buttons == RawMouseButtonFlags.MouseHorizontalWheel) + OnMouseScrollDataReceived(device, MouseScrollDirection.Horizontal, mouseData.Mouse.ButtonData); + return; + } + + // Button presses + MouseButton button = MouseButton.Left; + bool isDown = false; + + // Left + if (DetermineMouseButton(mouseData, RawMouseButtonFlags.LeftButtonDown, RawMouseButtonFlags.LeftButtonUp, ref isDown)) + button = MouseButton.Left; + // Middle + else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.MiddleButtonDown, RawMouseButtonFlags.MiddleButtonUp, ref isDown)) + button = MouseButton.Middle; + // Right + else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.RightButtonDown, RawMouseButtonFlags.RightButtonUp, ref isDown)) + button = MouseButton.Right; + // Button 4 + else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.Button4Down, RawMouseButtonFlags.Button4Up, ref isDown)) + button = MouseButton.Button4; + else if (DetermineMouseButton(mouseData, RawMouseButtonFlags.Button5Down, RawMouseButtonFlags.Button5Up, ref isDown)) + button = MouseButton.Button5; + + OnMouseButtonDataReceived(device, button, isDown); + } + + private bool DetermineMouseButton(RawInputMouseData data, RawMouseButtonFlags downButton, RawMouseButtonFlags upButton, ref bool isDown) + { + if (data.Mouse.Buttons == downButton || data.Mouse.Buttons == upButton) + { + isDown = data.Mouse.Buttons == downButton; + return true; + } + + isDown = false; + return false; + } + + #endregion + + #region Native + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetCursorPos(ref Win32Point pt); + + [StructLayout(LayoutKind.Sequential)] + private struct Win32Point + { + public readonly int X; + public readonly int Y; + } + + private static Win32Point GetCursorPosition() + { + Win32Point w32Mouse = new(); + GetCursorPos(ref w32Mouse); + + return w32Mouse; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/Utilities/InputUtilities.cs b/src/Avalonia/Artemis.UI.Windows/Utilities/InputUtilities.cs new file mode 100644 index 000000000..04d256df1 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Utilities/InputUtilities.cs @@ -0,0 +1,1400 @@ +using System; +using System.Runtime.InteropServices; +using Artemis.Core.Services; +using Microsoft.Win32; + +namespace Artemis.UI.Windows.Utilities +{ + /// + /// Provides static methods to convert between Win32 VirtualKeys + /// and our Key enum. + /// + public static class InputUtilities + { + [Flags] + private enum KeyStates + { + None = 0, + Down = 1, + Toggled = 2 + } + + [DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)] + private static extern short GetKeyState(int keyCode); + + private static KeyStates GetKeyState(KeyboardKey key) + { + KeyStates state = KeyStates.None; + + short retVal = GetKeyState(VirtualKeyFromKey(key)); + + //If the high-order bit is 1, the key is down + //otherwise, it is up. + if ((retVal & 0x8000) == 0x8000) + state |= KeyStates.Down; + + //If the low-order bit is 1, the key is toggled. + if ((retVal & 1) == 1) + state |= KeyStates.Toggled; + + return state; + } + + public static bool IsKeyDown(KeyboardKey key) + { + return KeyStates.Down == (GetKeyState(key) & KeyStates.Down); + } + + public static bool IsKeyToggled(KeyboardKey key) + { + return KeyStates.Toggled == (GetKeyState(key) & KeyStates.Toggled); + } + + /// + /// Convert a Win32 VirtualKey into our Key enum. + /// + public static KeyboardKey KeyFromVirtualKey(int virtualKey) + { + KeyboardKey key = KeyboardKey.None; + + switch (virtualKey) + { + case NativeMethods.VK_CANCEL: + key = KeyboardKey.Cancel; + break; + + case NativeMethods.VK_BACK: + key = KeyboardKey.Back; + break; + + case NativeMethods.VK_TAB: + key = KeyboardKey.Tab; + break; + + case NativeMethods.VK_CLEAR: + key = KeyboardKey.Clear; + break; + + case NativeMethods.VK_RETURN: + key = KeyboardKey.Return; + break; + + case NativeMethods.VK_PAUSE: + key = KeyboardKey.Pause; + break; + + case NativeMethods.VK_CAPSLOCK: + key = KeyboardKey.CapsLock; + break; + + case NativeMethods.VK_JUNJA: + key = KeyboardKey.JunjaMode; + break; + + case NativeMethods.VK_FINAL: + key = KeyboardKey.FinalMode; + break; + + case NativeMethods.VK_ESCAPE: + key = KeyboardKey.Escape; + break; + + case NativeMethods.VK_CONVERT: + key = KeyboardKey.ImeConvert; + break; + + case NativeMethods.VK_NONCONVERT: + key = KeyboardKey.ImeNonConvert; + break; + + case NativeMethods.VK_ACCEPT: + key = KeyboardKey.ImeAccept; + break; + + case NativeMethods.VK_MODECHANGE: + key = KeyboardKey.ImeModeChange; + break; + + case NativeMethods.VK_SPACE: + key = KeyboardKey.Space; + break; + + case NativeMethods.VK_PRIOR: + key = KeyboardKey.Prior; + break; + + case NativeMethods.VK_NEXT: + key = KeyboardKey.Next; + break; + + case NativeMethods.VK_END: + key = KeyboardKey.End; + break; + + case NativeMethods.VK_HOME: + key = KeyboardKey.Home; + break; + + case NativeMethods.VK_LEFT: + key = KeyboardKey.Left; + break; + + case NativeMethods.VK_UP: + key = KeyboardKey.Up; + break; + + case NativeMethods.VK_RIGHT: + key = KeyboardKey.Right; + break; + + case NativeMethods.VK_DOWN: + key = KeyboardKey.Down; + break; + + case NativeMethods.VK_SELECT: + key = KeyboardKey.Select; + break; + + case NativeMethods.VK_PRINT: + key = KeyboardKey.Print; + break; + + case NativeMethods.VK_EXECUTE: + key = KeyboardKey.Execute; + break; + + case NativeMethods.VK_INSERT: + key = KeyboardKey.Insert; + break; + + case NativeMethods.VK_DELETE: + key = KeyboardKey.Delete; + break; + + case NativeMethods.VK_HELP: + key = KeyboardKey.Help; + break; + + case NativeMethods.VK_0: + key = KeyboardKey.D0; + break; + + case NativeMethods.VK_1: + key = KeyboardKey.D1; + break; + + case NativeMethods.VK_2: + key = KeyboardKey.D2; + break; + + case NativeMethods.VK_3: + key = KeyboardKey.D3; + break; + + case NativeMethods.VK_4: + key = KeyboardKey.D4; + break; + + case NativeMethods.VK_5: + key = KeyboardKey.D5; + break; + + case NativeMethods.VK_6: + key = KeyboardKey.D6; + break; + + case NativeMethods.VK_7: + key = KeyboardKey.D7; + break; + + case NativeMethods.VK_8: + key = KeyboardKey.D8; + break; + + case NativeMethods.VK_9: + key = KeyboardKey.D9; + break; + + case NativeMethods.VK_A: + key = KeyboardKey.A; + break; + + case NativeMethods.VK_B: + key = KeyboardKey.B; + break; + + case NativeMethods.VK_C: + key = KeyboardKey.C; + break; + + case NativeMethods.VK_D: + key = KeyboardKey.D; + break; + + case NativeMethods.VK_E: + key = KeyboardKey.E; + break; + + case NativeMethods.VK_F: + key = KeyboardKey.F; + break; + + case NativeMethods.VK_G: + key = KeyboardKey.G; + break; + + case NativeMethods.VK_H: + key = KeyboardKey.H; + break; + + case NativeMethods.VK_I: + key = KeyboardKey.I; + break; + + case NativeMethods.VK_J: + key = KeyboardKey.J; + break; + + case NativeMethods.VK_K: + key = KeyboardKey.K; + break; + + case NativeMethods.VK_L: + key = KeyboardKey.L; + break; + + case NativeMethods.VK_M: + key = KeyboardKey.M; + break; + + case NativeMethods.VK_N: + key = KeyboardKey.N; + break; + + case NativeMethods.VK_O: + key = KeyboardKey.O; + break; + + case NativeMethods.VK_P: + key = KeyboardKey.P; + break; + + case NativeMethods.VK_Q: + key = KeyboardKey.Q; + break; + + case NativeMethods.VK_R: + key = KeyboardKey.R; + break; + + case NativeMethods.VK_S: + key = KeyboardKey.S; + break; + + case NativeMethods.VK_T: + key = KeyboardKey.T; + break; + + case NativeMethods.VK_U: + key = KeyboardKey.U; + break; + + case NativeMethods.VK_V: + key = KeyboardKey.V; + break; + + case NativeMethods.VK_W: + key = KeyboardKey.W; + break; + + case NativeMethods.VK_X: + key = KeyboardKey.X; + break; + + case NativeMethods.VK_Y: + key = KeyboardKey.Y; + break; + + case NativeMethods.VK_Z: + key = KeyboardKey.Z; + break; + + case NativeMethods.VK_LWIN: + key = KeyboardKey.LWin; + break; + + case NativeMethods.VK_RWIN: + key = KeyboardKey.RWin; + break; + + case NativeMethods.VK_APPS: + key = KeyboardKey.Apps; + break; + + case NativeMethods.VK_SLEEP: + key = KeyboardKey.Sleep; + break; + + case NativeMethods.VK_NUMPAD0: + key = KeyboardKey.NumPad0; + break; + + case NativeMethods.VK_NUMPAD1: + key = KeyboardKey.NumPad1; + break; + + case NativeMethods.VK_NUMPAD2: + key = KeyboardKey.NumPad2; + break; + + case NativeMethods.VK_NUMPAD3: + key = KeyboardKey.NumPad3; + break; + + case NativeMethods.VK_NUMPAD4: + key = KeyboardKey.NumPad4; + break; + + case NativeMethods.VK_NUMPAD5: + key = KeyboardKey.NumPad5; + break; + + case NativeMethods.VK_NUMPAD6: + key = KeyboardKey.NumPad6; + break; + + case NativeMethods.VK_NUMPAD7: + key = KeyboardKey.NumPad7; + break; + + case NativeMethods.VK_NUMPAD8: + key = KeyboardKey.NumPad8; + break; + + case NativeMethods.VK_NUMPAD9: + key = KeyboardKey.NumPad9; + break; + + case NativeMethods.VK_MULTIPLY: + key = KeyboardKey.Multiply; + break; + + case NativeMethods.VK_ADD: + key = KeyboardKey.Add; + break; + + case NativeMethods.VK_SEPARATOR: + key = KeyboardKey.Separator; + break; + + case NativeMethods.VK_SUBTRACT: + key = KeyboardKey.Subtract; + break; + + case NativeMethods.VK_DECIMAL: + key = KeyboardKey.Decimal; + break; + + case NativeMethods.VK_DIVIDE: + key = KeyboardKey.Divide; + break; + + case NativeMethods.VK_F1: + key = KeyboardKey.F1; + break; + + case NativeMethods.VK_F2: + key = KeyboardKey.F2; + break; + + case NativeMethods.VK_F3: + key = KeyboardKey.F3; + break; + + case NativeMethods.VK_F4: + key = KeyboardKey.F4; + break; + + case NativeMethods.VK_F5: + key = KeyboardKey.F5; + break; + + case NativeMethods.VK_F6: + key = KeyboardKey.F6; + break; + + case NativeMethods.VK_F7: + key = KeyboardKey.F7; + break; + + case NativeMethods.VK_F8: + key = KeyboardKey.F8; + break; + + case NativeMethods.VK_F9: + key = KeyboardKey.F9; + break; + + case NativeMethods.VK_F10: + key = KeyboardKey.F10; + break; + + case NativeMethods.VK_F11: + key = KeyboardKey.F11; + break; + + case NativeMethods.VK_F12: + key = KeyboardKey.F12; + break; + + case NativeMethods.VK_F13: + key = KeyboardKey.F13; + break; + + case NativeMethods.VK_F14: + key = KeyboardKey.F14; + break; + + case NativeMethods.VK_F15: + key = KeyboardKey.F15; + break; + + case NativeMethods.VK_F16: + key = KeyboardKey.F16; + break; + + case NativeMethods.VK_F17: + key = KeyboardKey.F17; + break; + + case NativeMethods.VK_F18: + key = KeyboardKey.F18; + break; + + case NativeMethods.VK_F19: + key = KeyboardKey.F19; + break; + + case NativeMethods.VK_F20: + key = KeyboardKey.F20; + break; + + case NativeMethods.VK_F21: + key = KeyboardKey.F21; + break; + + case NativeMethods.VK_F22: + key = KeyboardKey.F22; + break; + + case NativeMethods.VK_F23: + key = KeyboardKey.F23; + break; + + case NativeMethods.VK_F24: + key = KeyboardKey.F24; + break; + + case NativeMethods.VK_NUMLOCK: + key = KeyboardKey.NumLock; + break; + + case NativeMethods.VK_SCROLL: + key = KeyboardKey.Scroll; + break; + + case NativeMethods.VK_SHIFT: + case NativeMethods.VK_LSHIFT: + key = KeyboardKey.LeftShift; + break; + + case NativeMethods.VK_RSHIFT: + key = KeyboardKey.RightShift; + break; + + case NativeMethods.VK_CONTROL: + case NativeMethods.VK_LCONTROL: + key = KeyboardKey.LeftCtrl; + break; + + case NativeMethods.VK_RCONTROL: + key = KeyboardKey.RightCtrl; + break; + + case NativeMethods.VK_MENU: + case NativeMethods.VK_LMENU: + key = KeyboardKey.LeftAlt; + break; + + case NativeMethods.VK_RMENU: + key = KeyboardKey.RightAlt; + break; + + case NativeMethods.VK_BROWSER_BACK: + key = KeyboardKey.BrowserBack; + break; + + case NativeMethods.VK_BROWSER_FORWARD: + key = KeyboardKey.BrowserForward; + break; + + case NativeMethods.VK_BROWSER_REFRESH: + key = KeyboardKey.BrowserRefresh; + break; + + case NativeMethods.VK_BROWSER_STOP: + key = KeyboardKey.BrowserStop; + break; + + case NativeMethods.VK_BROWSER_SEARCH: + key = KeyboardKey.BrowserSearch; + break; + + case NativeMethods.VK_BROWSER_FAVORITES: + key = KeyboardKey.BrowserFavorites; + break; + + case NativeMethods.VK_BROWSER_HOME: + key = KeyboardKey.BrowserHome; + break; + + case NativeMethods.VK_VOLUME_MUTE: + key = KeyboardKey.VolumeMute; + break; + + case NativeMethods.VK_VOLUME_DOWN: + key = KeyboardKey.VolumeDown; + break; + + case NativeMethods.VK_VOLUME_UP: + key = KeyboardKey.VolumeUp; + break; + + case NativeMethods.VK_MEDIA_NEXT_TRACK: + key = KeyboardKey.MediaNextTrack; + break; + + case NativeMethods.VK_MEDIA_PREV_TRACK: + key = KeyboardKey.MediaPreviousTrack; + break; + + case NativeMethods.VK_MEDIA_STOP: + key = KeyboardKey.MediaStop; + break; + + case NativeMethods.VK_MEDIA_PLAY_PAUSE: + key = KeyboardKey.MediaPlayPause; + break; + + case NativeMethods.VK_LAUNCH_MAIL: + key = KeyboardKey.LaunchMail; + break; + + case NativeMethods.VK_LAUNCH_MEDIA_SELECT: + key = KeyboardKey.SelectMedia; + break; + + case NativeMethods.VK_LAUNCH_APP1: + key = KeyboardKey.LaunchApplication1; + break; + + case NativeMethods.VK_LAUNCH_APP2: + key = KeyboardKey.LaunchApplication2; + break; + + case NativeMethods.VK_OEM_1: + key = KeyboardKey.OemSemicolon; + break; + + case NativeMethods.VK_OEM_PLUS: + key = KeyboardKey.OemPlus; + break; + + case NativeMethods.VK_OEM_COMMA: + key = KeyboardKey.OemComma; + break; + + case NativeMethods.VK_OEM_MINUS: + key = KeyboardKey.OemMinus; + break; + + case NativeMethods.VK_OEM_PERIOD: + key = KeyboardKey.OemPeriod; + break; + + case NativeMethods.VK_OEM_2: + key = KeyboardKey.OemQuestion; + break; + + case NativeMethods.VK_OEM_3: + key = KeyboardKey.OemTilde; + break; + + + case NativeMethods.VK_OEM_4: + key = KeyboardKey.OemOpenBrackets; + break; + + case NativeMethods.VK_OEM_5: + key = KeyboardKey.OemPipe; + break; + + case NativeMethods.VK_OEM_6: + key = KeyboardKey.OemCloseBrackets; + break; + + case NativeMethods.VK_OEM_7: + key = KeyboardKey.OemQuotes; + break; + + case NativeMethods.VK_OEM_102: + key = KeyboardKey.OemBackslash; + break; + + case NativeMethods.VK_PROCESSKEY: + key = KeyboardKey.ImeProcessed; + break; + + case NativeMethods.VK_OEM_ATTN: // VK_DBE_ALPHANUMERIC + key = KeyboardKey.OemAttn; // DbeAlphanumeric + break; + + case NativeMethods.VK_OEM_FINISH: // VK_DBE_KATAKANA + key = KeyboardKey.OemFinish; // DbeKatakana + break; + + case NativeMethods.VK_OEM_COPY: // VK_DBE_HIRAGANA + key = KeyboardKey.OemCopy; // DbeHiragana + break; + + case NativeMethods.VK_OEM_AUTO: // VK_DBE_SBCSCHAR + key = KeyboardKey.OemAuto; // DbeSbcsChar + break; + + case NativeMethods.VK_OEM_ENLW: // VK_DBE_DBCSCHAR + key = KeyboardKey.OemEnlw; // DbeDbcsChar + break; + + case NativeMethods.VK_OEM_BACKTAB: // VK_DBE_ROMAN + key = KeyboardKey.OemBackTab; // DbeRoman + break; + + case NativeMethods.VK_ATTN: // VK_DBE_NOROMAN + key = KeyboardKey.Attn; // DbeNoRoman + break; + + case NativeMethods.VK_CRSEL: // VK_DBE_ENTERWORDREGISTERMODE + key = KeyboardKey.CrSel; // DbeEnterWordRegisterMode + break; + + case NativeMethods.VK_EXSEL: // VK_DBE_ENTERIMECONFIGMODE + key = KeyboardKey.ExSel; // DbeEnterImeConfigMode + break; + + case NativeMethods.VK_EREOF: // VK_DBE_FLUSHSTRING + key = KeyboardKey.EraseEof; // DbeFlushString + break; + + case NativeMethods.VK_PLAY: // VK_DBE_CODEINPUT + key = KeyboardKey.Play; // DbeCodeInput + break; + + case NativeMethods.VK_ZOOM: // VK_DBE_NOCODEINPUT + key = KeyboardKey.Zoom; // DbeNoCodeInput + break; + + case NativeMethods.VK_NONAME: // VK_DBE_DETERMINESTRING + key = KeyboardKey.NoName; // DbeDetermineString + break; + + case NativeMethods.VK_PA1: // VK_DBE_ENTERDLGCONVERSIONMODE + key = KeyboardKey.Pa1; // DbeEnterDlgConversionMode + break; + + case NativeMethods.VK_OEM_CLEAR: + key = KeyboardKey.OemClear; + break; + + default: + key = KeyboardKey.None; + break; + } + + return key; + } + + /// + /// Convert our Key enum into a Win32 VirtualKeyboardKey. + /// + public static int VirtualKeyFromKey(KeyboardKey key) + { + int virtualKey = 0; + + switch (key) + { + case KeyboardKey.Cancel: + virtualKey = NativeMethods.VK_CANCEL; + break; + + case KeyboardKey.Back: + virtualKey = NativeMethods.VK_BACK; + break; + + case KeyboardKey.Tab: + virtualKey = NativeMethods.VK_TAB; + break; + + case KeyboardKey.Clear: + virtualKey = NativeMethods.VK_CLEAR; + break; + + case KeyboardKey.Return: + virtualKey = NativeMethods.VK_RETURN; + break; + + case KeyboardKey.Pause: + virtualKey = NativeMethods.VK_PAUSE; + break; + + case KeyboardKey.CapsLock: + virtualKey = NativeMethods.VK_CAPSLOCK; + break; + + case KeyboardKey.JunjaMode: + virtualKey = NativeMethods.VK_JUNJA; + break; + + case KeyboardKey.FinalMode: + virtualKey = NativeMethods.VK_FINAL; + break; + + case KeyboardKey.Escape: + virtualKey = NativeMethods.VK_ESCAPE; + break; + + case KeyboardKey.ImeConvert: + virtualKey = NativeMethods.VK_CONVERT; + break; + + case KeyboardKey.ImeNonConvert: + virtualKey = NativeMethods.VK_NONCONVERT; + break; + + case KeyboardKey.ImeAccept: + virtualKey = NativeMethods.VK_ACCEPT; + break; + + case KeyboardKey.ImeModeChange: + virtualKey = NativeMethods.VK_MODECHANGE; + break; + + case KeyboardKey.Space: + virtualKey = NativeMethods.VK_SPACE; + break; + + case KeyboardKey.Prior: + virtualKey = NativeMethods.VK_PRIOR; + break; + + case KeyboardKey.Next: + virtualKey = NativeMethods.VK_NEXT; + break; + + case KeyboardKey.End: + virtualKey = NativeMethods.VK_END; + break; + + case KeyboardKey.Home: + virtualKey = NativeMethods.VK_HOME; + break; + + case KeyboardKey.Left: + virtualKey = NativeMethods.VK_LEFT; + break; + + case KeyboardKey.Up: + virtualKey = NativeMethods.VK_UP; + break; + + case KeyboardKey.Right: + virtualKey = NativeMethods.VK_RIGHT; + break; + + case KeyboardKey.Down: + virtualKey = NativeMethods.VK_DOWN; + break; + + case KeyboardKey.Select: + virtualKey = NativeMethods.VK_SELECT; + break; + + case KeyboardKey.Print: + virtualKey = NativeMethods.VK_PRINT; + break; + + case KeyboardKey.Execute: + virtualKey = NativeMethods.VK_EXECUTE; + break; + + case KeyboardKey.Insert: + virtualKey = NativeMethods.VK_INSERT; + break; + + case KeyboardKey.Delete: + virtualKey = NativeMethods.VK_DELETE; + break; + + case KeyboardKey.Help: + virtualKey = NativeMethods.VK_HELP; + break; + + case KeyboardKey.D0: + virtualKey = NativeMethods.VK_0; + break; + + case KeyboardKey.D1: + virtualKey = NativeMethods.VK_1; + break; + + case KeyboardKey.D2: + virtualKey = NativeMethods.VK_2; + break; + + case KeyboardKey.D3: + virtualKey = NativeMethods.VK_3; + break; + + case KeyboardKey.D4: + virtualKey = NativeMethods.VK_4; + break; + + case KeyboardKey.D5: + virtualKey = NativeMethods.VK_5; + break; + + case KeyboardKey.D6: + virtualKey = NativeMethods.VK_6; + break; + + case KeyboardKey.D7: + virtualKey = NativeMethods.VK_7; + break; + + case KeyboardKey.D8: + virtualKey = NativeMethods.VK_8; + break; + + case KeyboardKey.D9: + virtualKey = NativeMethods.VK_9; + break; + + case KeyboardKey.A: + virtualKey = NativeMethods.VK_A; + break; + + case KeyboardKey.B: + virtualKey = NativeMethods.VK_B; + break; + + case KeyboardKey.C: + virtualKey = NativeMethods.VK_C; + break; + + case KeyboardKey.D: + virtualKey = NativeMethods.VK_D; + break; + + case KeyboardKey.E: + virtualKey = NativeMethods.VK_E; + break; + + case KeyboardKey.F: + virtualKey = NativeMethods.VK_F; + break; + + case KeyboardKey.G: + virtualKey = NativeMethods.VK_G; + break; + + case KeyboardKey.H: + virtualKey = NativeMethods.VK_H; + break; + + case KeyboardKey.I: + virtualKey = NativeMethods.VK_I; + break; + + case KeyboardKey.J: + virtualKey = NativeMethods.VK_J; + break; + + case KeyboardKey.K: + virtualKey = NativeMethods.VK_K; + break; + + case KeyboardKey.L: + virtualKey = NativeMethods.VK_L; + break; + + case KeyboardKey.M: + virtualKey = NativeMethods.VK_M; + break; + + case KeyboardKey.N: + virtualKey = NativeMethods.VK_N; + break; + + case KeyboardKey.O: + virtualKey = NativeMethods.VK_O; + break; + + case KeyboardKey.P: + virtualKey = NativeMethods.VK_P; + break; + + case KeyboardKey.Q: + virtualKey = NativeMethods.VK_Q; + break; + + case KeyboardKey.R: + virtualKey = NativeMethods.VK_R; + break; + + case KeyboardKey.S: + virtualKey = NativeMethods.VK_S; + break; + + case KeyboardKey.T: + virtualKey = NativeMethods.VK_T; + break; + + case KeyboardKey.U: + virtualKey = NativeMethods.VK_U; + break; + + case KeyboardKey.V: + virtualKey = NativeMethods.VK_V; + break; + + case KeyboardKey.W: + virtualKey = NativeMethods.VK_W; + break; + + case KeyboardKey.X: + virtualKey = NativeMethods.VK_X; + break; + + case KeyboardKey.Y: + virtualKey = NativeMethods.VK_Y; + break; + + case KeyboardKey.Z: + virtualKey = NativeMethods.VK_Z; + break; + + case KeyboardKey.LWin: + virtualKey = NativeMethods.VK_LWIN; + break; + + case KeyboardKey.RWin: + virtualKey = NativeMethods.VK_RWIN; + break; + + case KeyboardKey.Apps: + virtualKey = NativeMethods.VK_APPS; + break; + + case KeyboardKey.Sleep: + virtualKey = NativeMethods.VK_SLEEP; + break; + + case KeyboardKey.NumPad0: + virtualKey = NativeMethods.VK_NUMPAD0; + break; + + case KeyboardKey.NumPad1: + virtualKey = NativeMethods.VK_NUMPAD1; + break; + + case KeyboardKey.NumPad2: + virtualKey = NativeMethods.VK_NUMPAD2; + break; + + case KeyboardKey.NumPad3: + virtualKey = NativeMethods.VK_NUMPAD3; + break; + + case KeyboardKey.NumPad4: + virtualKey = NativeMethods.VK_NUMPAD4; + break; + + case KeyboardKey.NumPad5: + virtualKey = NativeMethods.VK_NUMPAD5; + break; + + case KeyboardKey.NumPad6: + virtualKey = NativeMethods.VK_NUMPAD6; + break; + + case KeyboardKey.NumPad7: + virtualKey = NativeMethods.VK_NUMPAD7; + break; + + case KeyboardKey.NumPad8: + virtualKey = NativeMethods.VK_NUMPAD8; + break; + + case KeyboardKey.NumPad9: + virtualKey = NativeMethods.VK_NUMPAD9; + break; + + case KeyboardKey.Multiply: + virtualKey = NativeMethods.VK_MULTIPLY; + break; + + case KeyboardKey.Add: + virtualKey = NativeMethods.VK_ADD; + break; + + case KeyboardKey.Separator: + virtualKey = NativeMethods.VK_SEPARATOR; + break; + + case KeyboardKey.Subtract: + virtualKey = NativeMethods.VK_SUBTRACT; + break; + + case KeyboardKey.Decimal: + virtualKey = NativeMethods.VK_DECIMAL; + break; + + case KeyboardKey.Divide: + virtualKey = NativeMethods.VK_DIVIDE; + break; + + case KeyboardKey.F1: + virtualKey = NativeMethods.VK_F1; + break; + + case KeyboardKey.F2: + virtualKey = NativeMethods.VK_F2; + break; + + case KeyboardKey.F3: + virtualKey = NativeMethods.VK_F3; + break; + + case KeyboardKey.F4: + virtualKey = NativeMethods.VK_F4; + break; + + case KeyboardKey.F5: + virtualKey = NativeMethods.VK_F5; + break; + + case KeyboardKey.F6: + virtualKey = NativeMethods.VK_F6; + break; + + case KeyboardKey.F7: + virtualKey = NativeMethods.VK_F7; + break; + + case KeyboardKey.F8: + virtualKey = NativeMethods.VK_F8; + break; + + case KeyboardKey.F9: + virtualKey = NativeMethods.VK_F9; + break; + + case KeyboardKey.F10: + virtualKey = NativeMethods.VK_F10; + break; + + case KeyboardKey.F11: + virtualKey = NativeMethods.VK_F11; + break; + + case KeyboardKey.F12: + virtualKey = NativeMethods.VK_F12; + break; + + case KeyboardKey.F13: + virtualKey = NativeMethods.VK_F13; + break; + + case KeyboardKey.F14: + virtualKey = NativeMethods.VK_F14; + break; + + case KeyboardKey.F15: + virtualKey = NativeMethods.VK_F15; + break; + + case KeyboardKey.F16: + virtualKey = NativeMethods.VK_F16; + break; + + case KeyboardKey.F17: + virtualKey = NativeMethods.VK_F17; + break; + + case KeyboardKey.F18: + virtualKey = NativeMethods.VK_F18; + break; + + case KeyboardKey.F19: + virtualKey = NativeMethods.VK_F19; + break; + + case KeyboardKey.F20: + virtualKey = NativeMethods.VK_F20; + break; + + case KeyboardKey.F21: + virtualKey = NativeMethods.VK_F21; + break; + + case KeyboardKey.F22: + virtualKey = NativeMethods.VK_F22; + break; + + case KeyboardKey.F23: + virtualKey = NativeMethods.VK_F23; + break; + + case KeyboardKey.F24: + virtualKey = NativeMethods.VK_F24; + break; + + case KeyboardKey.NumLock: + virtualKey = NativeMethods.VK_NUMLOCK; + break; + + case KeyboardKey.Scroll: + virtualKey = NativeMethods.VK_SCROLL; + break; + + case KeyboardKey.LeftShift: + virtualKey = NativeMethods.VK_LSHIFT; + break; + + case KeyboardKey.RightShift: + virtualKey = NativeMethods.VK_RSHIFT; + break; + + case KeyboardKey.LeftCtrl: + virtualKey = NativeMethods.VK_LCONTROL; + break; + + case KeyboardKey.RightCtrl: + virtualKey = NativeMethods.VK_RCONTROL; + break; + + case KeyboardKey.LeftAlt: + virtualKey = NativeMethods.VK_LMENU; + break; + + case KeyboardKey.RightAlt: + virtualKey = NativeMethods.VK_RMENU; + break; + + case KeyboardKey.BrowserBack: + virtualKey = NativeMethods.VK_BROWSER_BACK; + break; + + case KeyboardKey.BrowserForward: + virtualKey = NativeMethods.VK_BROWSER_FORWARD; + break; + + case KeyboardKey.BrowserRefresh: + virtualKey = NativeMethods.VK_BROWSER_REFRESH; + break; + + case KeyboardKey.BrowserStop: + virtualKey = NativeMethods.VK_BROWSER_STOP; + break; + + case KeyboardKey.BrowserSearch: + virtualKey = NativeMethods.VK_BROWSER_SEARCH; + break; + + case KeyboardKey.BrowserFavorites: + virtualKey = NativeMethods.VK_BROWSER_FAVORITES; + break; + + case KeyboardKey.BrowserHome: + virtualKey = NativeMethods.VK_BROWSER_HOME; + break; + + case KeyboardKey.VolumeMute: + virtualKey = NativeMethods.VK_VOLUME_MUTE; + break; + + case KeyboardKey.VolumeDown: + virtualKey = NativeMethods.VK_VOLUME_DOWN; + break; + + case KeyboardKey.VolumeUp: + virtualKey = NativeMethods.VK_VOLUME_UP; + break; + + case KeyboardKey.MediaNextTrack: + virtualKey = NativeMethods.VK_MEDIA_NEXT_TRACK; + break; + + case KeyboardKey.MediaPreviousTrack: + virtualKey = NativeMethods.VK_MEDIA_PREV_TRACK; + break; + + case KeyboardKey.MediaStop: + virtualKey = NativeMethods.VK_MEDIA_STOP; + break; + + case KeyboardKey.MediaPlayPause: + virtualKey = NativeMethods.VK_MEDIA_PLAY_PAUSE; + break; + + case KeyboardKey.LaunchMail: + virtualKey = NativeMethods.VK_LAUNCH_MAIL; + break; + + case KeyboardKey.SelectMedia: + virtualKey = NativeMethods.VK_LAUNCH_MEDIA_SELECT; + break; + + case KeyboardKey.LaunchApplication1: + virtualKey = NativeMethods.VK_LAUNCH_APP1; + break; + + case KeyboardKey.LaunchApplication2: + virtualKey = NativeMethods.VK_LAUNCH_APP2; + break; + + case KeyboardKey.OemSemicolon: + virtualKey = NativeMethods.VK_OEM_1; + break; + + case KeyboardKey.OemPlus: + virtualKey = NativeMethods.VK_OEM_PLUS; + break; + + case KeyboardKey.OemComma: + virtualKey = NativeMethods.VK_OEM_COMMA; + break; + + case KeyboardKey.OemMinus: + virtualKey = NativeMethods.VK_OEM_MINUS; + break; + + case KeyboardKey.OemPeriod: + virtualKey = NativeMethods.VK_OEM_PERIOD; + break; + + case KeyboardKey.OemQuestion: + virtualKey = NativeMethods.VK_OEM_2; + break; + + case KeyboardKey.OemTilde: + virtualKey = NativeMethods.VK_OEM_3; + break; + + case KeyboardKey.OemOpenBrackets: + virtualKey = NativeMethods.VK_OEM_4; + break; + + case KeyboardKey.OemPipe: + virtualKey = NativeMethods.VK_OEM_5; + break; + + case KeyboardKey.OemCloseBrackets: + virtualKey = NativeMethods.VK_OEM_6; + break; + + case KeyboardKey.OemQuotes: + virtualKey = NativeMethods.VK_OEM_7; + break; + + case KeyboardKey.OemBackslash: + virtualKey = NativeMethods.VK_OEM_102; + break; + + case KeyboardKey.ImeProcessed: + virtualKey = NativeMethods.VK_PROCESSKEY; + break; + + case KeyboardKey.OemAttn: // DbeAlphanumeric + virtualKey = NativeMethods.VK_OEM_ATTN; // VK_DBE_ALPHANUMERIC + break; + + case KeyboardKey.OemFinish: // DbeKatakana + virtualKey = NativeMethods.VK_OEM_FINISH; // VK_DBE_KATAKANA + break; + + case KeyboardKey.OemCopy: // DbeHiragana + virtualKey = NativeMethods.VK_OEM_COPY; // VK_DBE_HIRAGANA + break; + + case KeyboardKey.OemAuto: // DbeSbcsChar + virtualKey = NativeMethods.VK_OEM_AUTO; // VK_DBE_SBCSCHAR + break; + + case KeyboardKey.OemEnlw: // DbeDbcsChar + virtualKey = NativeMethods.VK_OEM_ENLW; // VK_DBE_DBCSCHAR + break; + + case KeyboardKey.OemBackTab: // DbeRoman + virtualKey = NativeMethods.VK_OEM_BACKTAB; // VK_DBE_ROMAN + break; + + case KeyboardKey.Attn: // DbeNoRoman + virtualKey = NativeMethods.VK_ATTN; // VK_DBE_NOROMAN + break; + + case KeyboardKey.CrSel: // DbeEnterWordRegisterMode + virtualKey = NativeMethods.VK_CRSEL; // VK_DBE_ENTERWORDREGISTERMODE + break; + + case KeyboardKey.ExSel: // EnterImeConfigureMode + virtualKey = NativeMethods.VK_EXSEL; // VK_DBE_ENTERIMECONFIGMODE + break; + + case KeyboardKey.EraseEof: // DbeFlushString + virtualKey = NativeMethods.VK_EREOF; // VK_DBE_FLUSHSTRING + break; + + case KeyboardKey.Play: // DbeCodeInput + virtualKey = NativeMethods.VK_PLAY; // VK_DBE_CODEINPUT + break; + + case KeyboardKey.Zoom: // DbeNoCodeInput + virtualKey = NativeMethods.VK_ZOOM; // VK_DBE_NOCODEINPUT + break; + + case KeyboardKey.NoName: // DbeDetermineString + virtualKey = NativeMethods.VK_NONAME; // VK_DBE_DETERMINESTRING + break; + + case KeyboardKey.Pa1: // DbeEnterDlgConversionMode + virtualKey = NativeMethods.VK_PA1; // VK_ENTERDLGCONVERSIONMODE + break; + + case KeyboardKey.OemClear: + virtualKey = NativeMethods.VK_OEM_CLEAR; + break; + + case KeyboardKey.DeadCharProcessed: //This is usused. It's just here for completeness. + virtualKey = 0; //There is no Win32 VKey for this. + break; + + default: + virtualKey = 0; + break; + } + + return virtualKey; + } + } +} + diff --git a/src/Avalonia/Artemis.UI.Windows/Utilities/WindowUtilities.cs b/src/Avalonia/Artemis.UI.Windows/Utilities/WindowUtilities.cs new file mode 100644 index 000000000..1f45e44d0 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Utilities/WindowUtilities.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.InteropServices; + +namespace Artemis.UI.Windows.Utilities +{ + public static class WindowUtilities + { + public static int GetActiveProcessId() + { + // Get foreground window handle + IntPtr hWnd = GetForegroundWindow(); + + GetWindowThreadProcessId(hWnd, out uint processId); + return (int) processId; + } + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index e5f9d627d..c2d37b9eb 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -53,6 +53,33 @@ "System.Reactive": "5.0.0" } }, + "Avalonia.Win32": { + "type": "Direct", + "requested": "[0.10.10, )", + "resolved": "0.10.10", + "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "dependencies": { + "Avalonia": "0.10.10", + "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", + "System.Drawing.Common": "4.5.0", + "System.Numerics.Vectors": "4.5.0" + } + }, + "Microsoft.Win32": { + "type": "Direct", + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "V1I3Mvj0g6YFoVywl0uoPh7fJ4y7xTewBGrW9FN70dr9LruFnC4rJ/bJ2wxIvZyeVrsj2JaqxvkuSw9fhk9UKA==" + }, + "RawInput.Sharp": { + "type": "Direct", + "requested": "[0.0.4, )", + "resolved": "0.0.4", + "contentHash": "JAHC4oBBzvwvehjiWTUW0UAjptprQuX+21d/7LrsMAjBSrT9GB6q6JiEm0bQi69uTkBPohHNF7O8Wgcz3oVXkA==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", "resolved": "2.1.0.2020091801", @@ -122,17 +149,6 @@ "Svg.Skia": "0.5.10" } }, - "Avalonia.Win32": { - "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", - "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", - "System.Drawing.Common": "4.5.0", - "System.Numerics.Vectors": "4.5.0" - } - }, "Avalonia.X11": { "type": "Transitive", "resolved": "0.10.10", diff --git a/src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs b/src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs deleted file mode 100644 index 72a8d394b..000000000 --- a/src/Avalonia/Artemis.UI/Providers/AvaloniaInputProvider.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Artemis.Core.Services; - -namespace Artemis.UI.Providers -{ - public class AvaloniaInputProvider : InputProvider - { - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs index 07bc6b909..5d4428442 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs @@ -2,6 +2,7 @@ using System; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.UI.Shared.Events; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -16,6 +17,10 @@ namespace Artemis.UI.Screens.Debugger public DebugView() { InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + NavigationView navigation = this.Get("Navigation"); this.WhenActivated(d => diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml index 4e8fb46ef..4032e4ec9 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugView.axaml @@ -7,23 +7,25 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Debugger.Tabs.DataModel.DataModelDebugView"> - + - - Data Model - - On this page you can view the contents of the Artemis data model. - - - Please note that having this window open can have a performance impact on your system. - + + + Data Model + + On this page you can view the contents of the Artemis data model. + + + Please note that having this window open can have a performance impact on your system. + + - - - - + + + + @@ -35,9 +37,9 @@ + Text="{Binding DisplayValue}" + FontFamily="Consolas" + HorizontalAlignment="Right" /> @@ -50,9 +52,9 @@ + Text="{Binding CountDisplay, Mode=OneWay}" + FontFamily="Consolas" + HorizontalAlignment="Right" /> @@ -70,22 +72,20 @@ - + - [ - + ] - - List item [ - - ] - + + List item # + + - + @@ -99,20 +99,13 @@ + Text="{Binding CountDisplay, Mode=OneWay}" + FontFamily="Consolas" + HorizontalAlignment="Right" /> + + + - - - List item [ - - ] - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml index 15a902eed..a028787f8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml @@ -5,22 +5,25 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Debugger.Tabs.Performance.PerformanceDebugView"> - - Performance - - On this page you can see how much CPU time different plugin features are taking. If you are having performance issues, below you can find out which plugin might be the culprit. - - - - These performance stats are rather basic, for advanced performance profiling check out the wiki. + + + Performance + + On this page you can see how much CPU time different plugin features are taking. If you are having performance issues, below you can find out which plugin might be the culprit. - - JetBrains Profiling Guide - - + + + These performance stats are rather basic, for advanced performance profiling check out the wiki. + + + JetBrains Profiling Guide + + + + + + + + - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 177d5496a..68c7cd9be 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -4,11 +4,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.Home; using Artemis.UI.Screens.Root.Sidebar; -using Artemis.UI.Screens.Settings; -using Artemis.UI.Screens.SurfaceEditor; -using Artemis.UI.Screens.Workshop; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; @@ -18,21 +14,19 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform; using Avalonia.Threading; -using Ninject; -using Ninject.Parameters; using ReactiveUI; namespace Artemis.UI.Screens.Root { public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvider { - private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; - private readonly ICoreService _coreService; - private readonly ISettingsService _settingsService; - private readonly IWindowService _windowService; - private readonly IDebugService _debugService; private readonly IAssetLoader _assetLoader; + private readonly ICoreService _coreService; + private readonly IDebugService _debugService; + private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; + private readonly ISettingsService _settingsService; private readonly ISidebarVmFactory _sidebarVmFactory; + private readonly IWindowService _windowService; private SidebarViewModel? _sidebarViewModel; private TrayIcon? _trayIcon; private TrayIcons? _trayIcons; @@ -58,7 +52,6 @@ namespace Artemis.UI.Screens.Root coreService.StartupArguments = _lifeTime.Args.ToList(); mainWindowService.ConfigureMainWindowProvider(this); - registrationService.RegisterProviders(); DisplayAccordingToSettings(); Task.Run(coreService.Initialize); @@ -70,9 +63,6 @@ namespace Artemis.UI.Screens.Root set => this.RaiseAndSetIfChanged(ref _sidebarViewModel, value); } - /// - public RoutingState Router { get; } - private void CurrentMainWindowOnClosed(object? sender, EventArgs e) { _lifeTime.MainWindow = null; @@ -113,7 +103,7 @@ namespace Artemis.UI.Screens.Root { _trayIcon = new TrayIcon { - Icon = new WindowIcon(_assetLoader.Open(new Uri("avares://Artemis.UI/Assets/Images/Logo/bow.ico"))), + Icon = new WindowIcon(_assetLoader.Open(new Uri("avares://Artemis.UI/Assets/Images/Logo/bow.ico"))), Command = ReactiveCommand.Create(OpenMainWindow) }; _trayIcon.Menu = (NativeMenu?) Application.Current.FindResource("TrayIconMenu"); @@ -130,6 +120,9 @@ namespace Artemis.UI.Screens.Root _trayIcons = null; } + /// + public RoutingState Router { get; } + #region Tray commands public void OpenScreen(string displayName) diff --git a/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs b/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs index 86b0f2d50..4c45d8b28 100644 --- a/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs @@ -5,7 +5,6 @@ void RegisterBuiltInDataModelDisplays(); void RegisterBuiltInDataModelInputs(); void RegisterBuiltInPropertyEditors(); - void RegisterProviders(); void RegisterControllers(); void ApplyPreferredGraphicsContext(); } diff --git a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs index e6a1f9b4d..c2681436a 100644 --- a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs @@ -1,5 +1,4 @@ using Artemis.Core.Services; -using Artemis.UI.Providers; using Artemis.UI.Services.Interfaces; namespace Artemis.UI.Services @@ -24,11 +23,6 @@ namespace Artemis.UI.Services { } - public void RegisterProviders() - { - _inputService.AddInputProvider(new AvaloniaInputProvider()); - } - public void RegisterControllers() { } From ebed9f5560d24c31e7d5d6dc9214e702b7b0c435 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 7 Dec 2021 00:14:23 +0100 Subject: [PATCH 092/270] Core - Renamed ProfileConfigurationHotkey to Hotkey Avalonia - Added HotkeyBox --- ...rofileConfigurationHotkey.cs => Hotkey.cs} | 8 +- .../ProfileConfiguration.cs | 8 +- .../Input/Enums/KeyboardModifierKey.cs | 6 + .../Artemis.UI.Avalonia.Shared.xml | 30 +++++ .../ProfileConfigurationHotkeyViewModel.cs | 6 +- .../Artemis.UI.Shared.csproj | 3 + .../Controls/ArtemisIcon.axaml.cs | 76 ++++++----- .../Controls/HotkeyBox.axaml | 26 ++++ .../Controls/HotkeyBox.axaml.cs | 127 ++++++++++++++++++ .../Controls/NoInputTextBox.cs | 24 ++++ .../Screens/Workshop/WorkshopView.axaml | 3 + 11 files changed, 269 insertions(+), 48 deletions(-) rename src/Artemis.Core/Models/ProfileConfiguration/{ProfileConfigurationHotkey.cs => Hotkey.cs} (85%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/NoInputTextBox.cs diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationHotkey.cs b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs similarity index 85% rename from src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationHotkey.cs rename to src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs index 0a735f875..aaa5485e6 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationHotkey.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs @@ -6,18 +6,18 @@ namespace Artemis.Core /// /// Represents a key or combination of keys that changes the suspension status of a /// - public class ProfileConfigurationHotkey : CorePropertyChanged, IStorageModel + public class Hotkey : CorePropertyChanged, IStorageModel { /// - /// Creates a new instance of + /// Creates a new instance of /// - public ProfileConfigurationHotkey() + public Hotkey() { Entity = new ProfileConfigurationHotkeyEntity(); } - internal ProfileConfigurationHotkey(ProfileConfigurationHotkeyEntity entity) + internal Hotkey(ProfileConfigurationHotkeyEntity entity) { Entity = entity; Load(); diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index a278443cd..942f1af4c 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -104,12 +104,12 @@ namespace Artemis.Core /// /// Gets or sets the hotkey used to enable or toggle the profile /// - public ProfileConfigurationHotkey? EnableHotkey { get; set; } + public Hotkey? EnableHotkey { get; set; } /// /// Gets or sets the hotkey used to disable the profile /// - public ProfileConfigurationHotkey? DisableHotkey { get; set; } + public Hotkey? DisableHotkey { get; set; } /// /// Gets the ID of the profile of this profile configuration @@ -246,8 +246,8 @@ namespace Artemis.Core ? new NodeScript("Activate profile", "Whether or not the profile should be active", Entity.ActivationCondition, this) : new NodeScript("Activate profile", "Whether or not the profile should be active", this); - EnableHotkey = Entity.EnableHotkey != null ? new ProfileConfigurationHotkey(Entity.EnableHotkey) : null; - DisableHotkey = Entity.DisableHotkey != null ? new ProfileConfigurationHotkey(Entity.DisableHotkey) : null; + EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null; + DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null; } /// diff --git a/src/Artemis.Core/Services/Input/Enums/KeyboardModifierKey.cs b/src/Artemis.Core/Services/Input/Enums/KeyboardModifierKey.cs index 2ebde89e7..306822572 100644 --- a/src/Artemis.Core/Services/Input/Enums/KeyboardModifierKey.cs +++ b/src/Artemis.Core/Services/Input/Enums/KeyboardModifierKey.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; namespace Artemis.Core.Services { @@ -9,18 +10,23 @@ namespace Artemis.Core.Services public enum KeyboardModifierKey { /// No modifiers are pressed. + [Description("None")] None = 0, /// The ALT key. + [Description("Alt")] Alt = 1, /// The CTRL key. + [Description("Ctrl")] Control = 2, /// The SHIFT key. + [Description("Shift")] Shift = 4, /// The Windows logo key. + [Description("Win")] Windows = 8 } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 36b985041..3ee84f85d 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -96,6 +96,36 @@ + + + Represents a control that can be used to display or edit instances. + + + + + Creates a new instance of the class + + + + + Gets or sets the currently displayed icon as either a or an + pointing + to an SVG + + + + + Gets or sets the currently displayed icon as either a or an + pointing + to an SVG + + + + + + + + Gets or sets the to display diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileConfigurationHotkeyViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileConfigurationHotkeyViewModel.cs index 183447733..fbba87b5a 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileConfigurationHotkeyViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileConfigurationHotkeyViewModel.cs @@ -22,7 +22,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit UpdateHotkeyDisplay(); } - public ProfileConfigurationHotkey Hotkey => _isDisableHotkey ? _profileConfiguration.DisableHotkey : _profileConfiguration.EnableHotkey; + public Hotkey Hotkey => _isDisableHotkey ? _profileConfiguration.DisableHotkey : _profileConfiguration.EnableHotkey; public string Hint { @@ -66,13 +66,13 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit if (_isDisableHotkey) { - _profileConfiguration.DisableHotkey ??= new ProfileConfigurationHotkey(); + _profileConfiguration.DisableHotkey ??= new Hotkey(); _profileConfiguration.DisableHotkey.Key = (KeyboardKey?) e.Key; _profileConfiguration.DisableHotkey.Modifiers = (KeyboardModifierKey?) Keyboard.Modifiers; } else { - _profileConfiguration.EnableHotkey ??= new ProfileConfigurationHotkey(); + _profileConfiguration.EnableHotkey ??= new Hotkey(); _profileConfiguration.EnableHotkey.Key = (KeyboardKey?) e.Key; _profileConfiguration.EnableHotkey.Modifiers = (KeyboardModifierKey?) Keyboard.Modifiers; } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 7dbbc699f..180b48704 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -44,6 +44,9 @@ + + HotkeyBox.axaml + %(Filename) diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs index 8ea3417ad..cc68222dd 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs @@ -21,43 +21,7 @@ namespace Artemis.UI.Shared.Controls public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon), notifying: IconChanging); - private static void IconChanging(IAvaloniaObject sender, bool before) - { - if (before) - ((ArtemisIcon) sender).Update(); - } - - private void Update() - { - try - { - // First look for an enum value instead of a string - if (Icon is MaterialIconKind materialIcon) - Content = new MaterialIcon {Kind = materialIcon, Width = Bounds.Width, Height = Bounds.Height }; - // If it's a string there are several options - else if (Icon is string iconString) - { - // An enum defined as a string - if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon)) - Content = new MaterialIcon {Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height}; - // An URI pointing to an SVG - else if (iconString.EndsWith(".svg")) - { - SvgSource source = new(); - source.Load(iconString); - Content = new SvgImage {Source = source}; - } - // An URI pointing to a different kind of image - else - Content = new Image {Source = new Bitmap(iconString), Width = Bounds.Width, Height = Bounds.Height }; - } - } - catch - { - Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height }; - } - } - + /// /// Gets or sets the currently displayed icon as either a or an pointing /// to an SVG @@ -70,6 +34,44 @@ namespace Artemis.UI.Shared.Controls #endregion + private static void IconChanging(IAvaloniaObject sender, bool before) + { + if (before) + ((ArtemisIcon)sender).Update(); + } + + private void Update() + { + try + { + // First look for an enum value instead of a string + if (Icon is MaterialIconKind materialIcon) + Content = new MaterialIcon { Kind = materialIcon, Width = Bounds.Width, Height = Bounds.Height }; + // If it's a string there are several options + else if (Icon is string iconString) + { + // An enum defined as a string + if (Enum.TryParse(iconString, true, out MaterialIconKind parsedIcon)) + Content = new MaterialIcon { Kind = parsedIcon, Width = Bounds.Width, Height = Bounds.Height }; + // An URI pointing to an SVG + else if (iconString.EndsWith(".svg")) + { + SvgSource source = new(); + source.Load(iconString); + Content = new SvgImage { Source = source }; + } + // An URI pointing to a different kind of image + else + Content = new Image { Source = new Bitmap(iconString), Width = Bounds.Width, Height = Bounds.Height }; + } + } + catch + { + Content = new MaterialIcon { Kind = MaterialIconKind.QuestionMark, Width = Bounds.Width, Height = Bounds.Height }; + } + } + + public ArtemisIcon() { InitializeComponent(); diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml new file mode 100644 index 000000000..29ae38f38 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs new file mode 100644 index 000000000..a78ff19d5 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Humanizer; +using Material.Icons; + +namespace Artemis.UI.Shared.Controls +{ + /// + /// Represents a control that can be used to display or edit instances. + /// + public class HotkeyBox : UserControl + { + private readonly TextBox _displayTextBox; + + /// + /// Creates a new instance of the class + /// + public HotkeyBox() + { + InitializeComponent(); + + _displayTextBox = this.Find("DisplayTextBox"); + _displayTextBox.KeyDown += DisplayTextBoxOnKeyDown; + _displayTextBox.KeyUp += DisplayTextBoxOnKeyUp; + UpdateDisplayTextBox(); + } + + private static void HotkeyChanging(IAvaloniaObject sender, bool before) + { + ((HotkeyBox) sender).UpdateDisplayTextBox(); + } + + private void DisplayTextBoxOnKeyDown(object? sender, KeyEventArgs e) + { + if (e.Key >= Key.LeftShift && e.Key <= Key.RightAlt) + return; + + Hotkey ??= new Hotkey(); + Hotkey.Key = (KeyboardKey?) e.Key; + Hotkey.Modifiers = (KeyboardModifierKey?) e.KeyModifiers; + UpdateDisplayTextBox(); + + e.Handled = true; + } + + private void DisplayTextBoxOnKeyUp(object? sender, KeyEventArgs e) + { + if (e.KeyModifiers == KeyModifiers.None) + FocusManager.Instance.Focus(null); + + e.Handled = true; + } + + private void UpdateDisplayTextBox() + { + string? display = null; + if (Hotkey?.Modifiers != null) + display = string.Join("+", Enum.GetValues().Skip(1).Where(m => Hotkey.Modifiers.Value.HasFlag(m)).Select(v => v.Humanize())); + if (Hotkey?.Key != null) + display = string.IsNullOrEmpty(display) ? Hotkey.Key.ToString() : $"{display}+{Hotkey.Key}"; + + _displayTextBox.Text = display; + _displayTextBox.CaretIndex = _displayTextBox.Text?.Length ?? 0; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + Hotkey = null; + FocusManager.Instance.Focus(null); + + UpdateDisplayTextBox(); + } + + #region Properties + + /// + /// Gets or sets the currently displayed icon as either a or an + /// pointing + /// to an SVG + /// + public static readonly StyledProperty HotkeyProperty = + AvaloniaProperty.Register(nameof(Hotkey), notifying: HotkeyChanging); + + public static readonly StyledProperty WatermarkProperty = + AvaloniaProperty.Register(nameof(Watermark)); + + public static readonly StyledProperty UseFloatingWatermarkProperty = + AvaloniaProperty.Register(nameof(UseFloatingWatermark)); + + /// + /// Gets or sets the currently displayed icon as either a or an + /// pointing + /// to an SVG + /// + public Hotkey? Hotkey + { + get => GetValue(HotkeyProperty); + set => SetValue(HotkeyProperty, value); + } + + public string? Watermark + { + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); + } + + public bool UseFloatingWatermark + { + get => GetValue(UseFloatingWatermarkProperty); + set => SetValue(UseFloatingWatermarkProperty, value); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/NoInputTextBox.cs b/src/Avalonia/Artemis.UI.Shared/Controls/NoInputTextBox.cs new file mode 100644 index 000000000..6d20bb00e --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/NoInputTextBox.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Styling; + +namespace Artemis.UI.Shared.Controls +{ + internal class NoInputTextBox : TextBox, IStyleable + { + /// + protected override void OnKeyDown(KeyEventArgs e) + { + // Don't call the base method on purpose + } + + /// + protected override void OnKeyUp(KeyEventArgs e) + { + // Don't call the base method on purpose + } + + Type IStyleable.StyleKey => typeof(TextBox); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml index f46281660..6a71044e9 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:builders="clr-namespace:Artemis.UI.Shared.Services.Builders;assembly=Artemis.UI.Shared" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.WorkshopView"> @@ -22,6 +23,8 @@ + + From 971e32f7a51c8a692ea8e9e5043823ded2a0a4a5 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 8 Dec 2021 23:48:25 +0100 Subject: [PATCH 093/270] Plugin settings - Fix content dialogs not working in settings dialogs --- .../Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 65821f1e8..30caafb04 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -9,5 +9,7 @@ Width="800" Height="800" WindowStartupLocation="CenterOwner"> - + + + From ccdb0b9711d0e6cb097f6322e40f2a152c2872c7 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Thu, 9 Dec 2021 07:18:24 -0800 Subject: [PATCH 094/270] Linux - Added intiial input provider --- src/Avalonia/Artemis.UI.Linux/App.axaml.cs | 10 + .../Providers/Input/LinuxAbsoluteAxis.cs | 34 + .../Providers/Input/LinuxInputDevice.cs | 93 +++ .../Providers/Input/LinuxInputDeviceFinder.cs | 42 ++ .../Providers/Input/LinuxInputDeviceReader.cs | 60 ++ .../Providers/Input/LinuxInputEventArgs.cs | 12 + .../Providers/Input/LinuxInputEventType.cs | 68 ++ .../Providers/Input/LinuxInputProvider.cs | 119 ++++ .../Providers/Input/LinuxKeyboardKeyCodes.cs | 620 +++++++++++++++++ .../Providers/Input/LinuxRelativeAxis.cs | 16 + .../Utilities/InputUtilities.cs | 621 ++++++++++++++++++ 11 files changed, 1695 insertions(+) create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxAbsoluteAxis.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDevice.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceFinder.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventArgs.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventType.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxKeyboardKeyCodes.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxRelativeAxis.cs create mode 100644 src/Avalonia/Artemis.UI.Linux/Utilities/InputUtilities.cs diff --git a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs index 4d79f7afc..6a0f198c9 100644 --- a/src/Avalonia/Artemis.UI.Linux/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Linux/App.axaml.cs @@ -1,3 +1,5 @@ +using Artemis.Core.Services; +using Artemis.UI.Linux.Providers.Input; using Avalonia; using Avalonia.Markup.Xaml; using Avalonia.Threading; @@ -17,6 +19,8 @@ namespace Artemis.UI.Linux _kernel = ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); + + RegisterProviders(); } public override void OnFrameworkInitializationCompleted() @@ -25,5 +29,11 @@ namespace Artemis.UI.Linux if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); } + + private void RegisterProviders() + { + IInputService inputService = _kernel.Get(); + inputService.AddInputProvider(_kernel.Get()); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxAbsoluteAxis.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxAbsoluteAxis.cs new file mode 100644 index 000000000..60e3fbbb7 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxAbsoluteAxis.cs @@ -0,0 +1,34 @@ +namespace Artemis.UI.Linux.Providers.Input +{ + public enum LinuxAbsoluteAxis + { + ABS_X = 0x00, + ABS_Y = 0x01, + ABS_Z = 0x02, + ABS_RX = 0x03, + ABS_RY = 0x04, + ABS_RZ = 0x05, + ABS_THROTTLE = 0x06, + ABS_RUDDER = 0x07, + ABS_WHEEL = 0x08, + ABS_GAS = 0x09, + ABS_BRAKE = 0x0a, + ABS_HAT0X = 0x10, + ABS_HAT0Y = 0x11, + ABS_HAT1X = 0x12, + ABS_HAT1Y = 0x13, + ABS_HAT2X = 0x14, + ABS_HAT2Y = 0x15, + ABS_HAT3X = 0x16, + ABS_HAT3Y = 0x17, + ABS_PRESSURE = 0x18, + ABS_DISTANCE = 0x19, + ABS_TILT_X = 0x1a, + ABS_TILT_Y = 0x1b, + ABS_TOOL_WIDTH = 0x1c, + + ABS_VOLUME = 0x20, + + ABS_MISC = 0x28, + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDevice.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDevice.cs new file mode 100644 index 000000000..b944da4d4 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDevice.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using Humanizer; + +namespace Artemis.UI.Linux.Providers.Input +{ + /// + /// Data transfer object representing a device read from /proc/bus/input/devices + /// + public class LinuxInputDevice + { + public string InputId { get; } + public string? Bus { get; } + public string? Vendor { get; } + public string? Product { get; } + public string? Version { get; } + public string? Name { get; } + public string? Phys { get; } + public string? Sysfs { get; } + public string? Uniq { get; } + public string[]? Handlers { get; } + public bool IsMouse => Handlers.Any(h => h.Contains("mouse")); + public bool IsKeyboard => Handlers.Any(h => h.Contains("kbd")); + public bool IsGamePad => Handlers.Any(h => h.Contains("js")); + public string EventPath => $"/dev/input/{Handlers.First(h => h.Contains("event"))}"; + + public LinuxInputDevice(IEnumerable lines) + { + foreach (string line in lines) + { + char dataType = line.First(); + string data = line.Substring(3); + //get the first character in each line and set the according property with relevant data + switch (dataType) + { + case 'I': + InputId = data; + foreach (string component in data.Split(" ")) + { + string?[] parts = component.Split('='); + switch (parts[0]) + { + case "Bus": + Bus = parts[1]; + break; + case "Vendor": + Vendor = parts[1]; + break; + case "Product": + Product = parts[1]; + break; + case "Version": + Version = parts[1]; + break; + default: + break; + } + } + break; + case 'N': + Name = data.Replace("\"", "") + .Replace("Name=", ""); + break; + case 'P': + Phys = data.Replace("Phys=", ""); + break; + case 'S': + Sysfs = data.Replace("Sysfs=", ""); + break; + case 'H': + Handlers = data.Replace("Handlers=", "").Split(" "); + break; + case 'U': + Uniq = data.Replace("Uniq=", ""); + break; + default: + //do we need any more of this data? + break; + } + } + } + + #region Overrides of Object + + /// + public override string ToString() => $"{Name} - {EventPath}"; + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceFinder.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceFinder.cs new file mode 100644 index 000000000..7268b1ee4 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceFinder.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Artemis.UI.Linux.Providers.Input +{ + public static class LinuxInputDeviceFinder + { + private const string DEVICES_FILE = "/proc/bus/input/devices"; + + public static IEnumerable Find() + { + return File.ReadAllLines(DEVICES_FILE) + .PartitionBy(s => s == "") //split on empty lines + .Select(lineGroup => new LinuxInputDevice(lineGroup)); + } + + //https://stackoverflow.com/questions/56623354 + private static IEnumerable> PartitionBy(this IEnumerable a, Func predicate) + { + int groupNumber = 0; + Func getGroupNumber = skip => + { + if (skip) + { + // prepare next group, we don't care if we increment more than once + // we only want to split groups + groupNumber++; + // null, to be able to filter out group separators + return null; + } + return groupNumber; + }; + return a + .Select(x => new { Value = x, GroupNumber = getGroupNumber(predicate(x))} ) + .Where(x => x.GroupNumber != null) + .GroupBy(x => x.GroupNumber) + .Select(g => g.Select(x => x.Value)); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs new file mode 100644 index 000000000..81e4bd6c2 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputDeviceReader.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Linux.Utilities; + +namespace Artemis.UI.Linux.Providers.Input +{ + internal class LinuxInputDeviceReader + { + private readonly FileStream _stream; + private readonly Task _task; + private readonly CancellationTokenSource _cts; + private readonly byte[] _buffer; + internal event EventHandler? InputEvent; + + public LinuxInputDevice InputDevice { get; } + + public LinuxInputDeviceReader(LinuxInputDevice inputDevice) + { + InputDevice = inputDevice; + + _buffer = new byte[Marshal.SizeOf()]; + _stream = new FileStream(InputDevice.EventPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 2048, FileOptions.Asynchronous); + _cts = new CancellationTokenSource(); + _task = Task.Run(Read, _cts.Token); + } + + private async Task Read() + { + while (!_cts.IsCancellationRequested) + { + try + { + int readBytes = await _stream.ReadAsync(_buffer, _cts.Token); + if (readBytes == 0) + continue; + + InputEvent?.Invoke(this, MemoryMarshal.Read(_buffer)); + } + catch + { + // ignored + } + } + } + + public void Dispose() + { + _cts.Cancel(); + + _stream.Flush(); + _stream.Dispose(); + + //_task.Wait(); //TODO: fix this, it hangs + _cts.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventArgs.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventArgs.cs new file mode 100644 index 000000000..765d7b514 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventArgs.cs @@ -0,0 +1,12 @@ +namespace Artemis.UI.Linux.Providers.Input +{ + //https://www.kernel.org/doc/Documentation/input/input.txt + internal readonly struct LinuxInputEventArgs + { + internal readonly long TimeSeconds; + internal readonly long TimeMicroseconds; + internal readonly LinuxInputEventType Type; + internal readonly short Code; + internal readonly int Value; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventType.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventType.cs new file mode 100644 index 000000000..dadeb76aa --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputEventType.cs @@ -0,0 +1,68 @@ +namespace Artemis.UI.Linux.Providers.Input +{ + /// + /// https://www.kernel.org/doc/Documentation/input/event-codes.txt + /// + internal enum LinuxInputEventType : ushort + { + /// + /// Used as markers to separate events. Events may be separated in time or in space, such as with the multitouch protocol. + /// + SYN = 0x00, + + /// + /// Used to describe state changes of keyboards, buttons, or other key-like devices. + /// + KEY = 0x01, + + /// + /// Used to describe relative axis value changes, e.g. moving the mouse 5 units to the left. + /// + REL = 0x02, + + /// + /// Used to describe absolute axis value changes, e.g. describing the coordinates of a touch on a touchscreen. + /// + ABS = 0x03, + + /// + /// Used to describe miscellaneous input data that do not fit into other types. + /// + MSC = 0x04, + + /// + /// Used to describe binary state input switches. + /// + SW = 0x05, + + /// + /// Used to turn LEDs on devices on and off. + /// + LED = 0x11, + + /// + /// Used to output sound to devices. + /// + SND = 0x12, + + /// + /// Used for autorepeating devices. + /// + REP = 0x14, + + /// + /// Used to send force feedback commands to an input device. + /// + FF = 0x15, + + /// + /// A special type for power button and switch input. + /// + PWR = 0x16, + + /// + /// Used to receive force feedback device status. + /// + FF_STATUS = 0x17, + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs new file mode 100644 index 000000000..ea8baa816 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxInputProvider.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using Artemis.Core.Services; +using Artemis.UI.Linux.Utilities; +using Serilog; + +namespace Artemis.UI.Linux.Providers.Input +{ + public class LinuxInputProvider : InputProvider + { + private readonly IInputService _inputService; + private readonly ILogger _logger; + private readonly List _readers; + + public LinuxInputProvider(ILogger logger, IInputService inputService) + { + _logger = logger; + _inputService = inputService; + _readers = new List(); + + foreach (LinuxInputDevice deviceDefinition in LinuxInputDeviceFinder.Find()) + { + LinuxInputDeviceReader? reader = new LinuxInputDeviceReader(deviceDefinition); + reader.InputEvent += OnInputEvent; + _readers.Add(reader); + } + } + + private void OnInputEvent(object? sender, LinuxInputEventArgs e) + { + if (sender is not LinuxInputDeviceReader reader) + return; + + if (reader.InputDevice.IsKeyboard) + { + HandleKeyboardData(reader.InputDevice, e); + } + else if (reader.InputDevice.IsMouse) + { + HandleMouseData(reader.InputDevice, e); + } + else if (reader.InputDevice.IsGamePad) + { + //TODO: handle game pad input? + } + } + + private void HandleKeyboardData(LinuxInputDevice keyboard, LinuxInputEventArgs e) + { + switch (e.Type) + { + case LinuxInputEventType.KEY: + KeyboardKey key = InputUtilities.KeyFromKeyCode((LinuxKeyboardKeyCodes) e.Code); + bool isDown = e.Value != 0; + + _logger.Verbose($"Keyboard Key: {(LinuxKeyboardKeyCodes) e.Code} | Down: {isDown}"); + + //TODO: identify + + OnKeyboardDataReceived(null, key, isDown); + break; + default: + _logger.Verbose($"Unknown keyboard event type: {e.Type}"); + break; + } + } + + private void HandleMouseData(LinuxInputDevice mouse, LinuxInputEventArgs e) + { + switch (e.Type) + { + case LinuxInputEventType.KEY: + bool isDown = e.Value != 0; + MouseButton button = InputUtilities.MouseButtonFromButtonCode((LinuxKeyboardKeyCodes)e.Code); + + _logger.Verbose($"Mouse Button: {(LinuxKeyboardKeyCodes) e.Code} | Down: {isDown}"); + + //TODO: identify + + OnMouseButtonDataReceived(null, button, isDown); + break; + + case LinuxInputEventType.ABS: + LinuxAbsoluteAxis absoluteAxis = (LinuxAbsoluteAxis) e.Code; + _logger.Verbose($"Absolute mouse: axis={absoluteAxis} | value={e.Value}"); + break; + case LinuxInputEventType.REL: + LinuxRelativeAxis relativeAxis = (LinuxRelativeAxis) e.Code; + _logger.Verbose($"Relative mouse: axis={relativeAxis} | value={e.Value}"); + + //TODO: handle mouse movement + break; + default: + _logger.Verbose($"Unknown mouse event type: {e.Type}"); + break; + } + } + + #region Overrides of InputProvider + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + for (int i = _readers.Count - 1; i >= 0; i--) + { + _readers[i].InputEvent -= OnInputEvent; + _readers[i].Dispose(); + _readers.RemoveAt(i); + } + } + + base.Dispose(disposing); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxKeyboardKeyCodes.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxKeyboardKeyCodes.cs new file mode 100644 index 000000000..56a6f41ef --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxKeyboardKeyCodes.cs @@ -0,0 +1,620 @@ +namespace Artemis.UI.Linux.Providers.Input +{ + //https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h + public enum LinuxKeyboardKeyCodes + { + KEY_RESERVED = 0, + KEY_ESC = 1, + KEY_1 = 2, + KEY_2 = 3, + KEY_3 = 4, + KEY_4 = 5, + KEY_5 = 6, + KEY_6 = 7, + KEY_7 = 8, + KEY_8 = 9, + KEY_9 = 10, + KEY_0 = 11, + KEY_MINUS = 12, + KEY_EQUAL = 13, + KEY_BACKSPACE = 14, + KEY_TAB = 15, + KEY_Q = 16, + KEY_W = 17, + KEY_E = 18, + KEY_R = 19, + KEY_T = 20, + KEY_Y = 21, + KEY_U = 22, + KEY_I = 23, + KEY_O = 24, + KEY_P = 25, + KEY_LEFTBRACE = 26, + KEY_RIGHTBRACE = 27, + KEY_ENTER = 28, + KEY_LEFTCTRL = 29, + KEY_A = 30, + KEY_S = 31, + KEY_D = 32, + KEY_F = 33, + KEY_G = 34, + KEY_H = 35, + KEY_J = 36, + KEY_K = 37, + KEY_L = 38, + KEY_SEMICOLON = 39, + KEY_APOSTROPHE = 40, + KEY_GRAVE = 41, + KEY_LEFTSHIFT = 42, + KEY_BACKSLASH = 43, + KEY_Z = 44, + KEY_X = 45, + KEY_C = 46, + KEY_V = 47, + KEY_B = 48, + KEY_N = 49, + KEY_M = 50, + KEY_COMMA = 51, + KEY_DOT = 52, + KEY_SLASH = 53, + KEY_RIGHTSHIFT = 54, + KEY_KPASTERISK = 55, + KEY_LEFTALT = 56, + KEY_SPACE = 57, + KEY_CAPSLOCK = 58, + KEY_F1 = 59, + KEY_F2 = 60, + KEY_F3 = 61, + KEY_F4 = 62, + KEY_F5 = 63, + KEY_F6 = 64, + KEY_F7 = 65, + KEY_F8 = 66, + KEY_F9 = 67, + KEY_F10 = 68, + KEY_NUMLOCK = 69, + KEY_SCROLLLOCK = 70, + KEY_KP7 = 71, + KEY_KP8 = 72, + KEY_KP9 = 73, + KEY_KPMINUS = 74, + KEY_KP4 = 75, + KEY_KP5 = 76, + KEY_KP6 = 77, + KEY_KPPLUS = 78, + KEY_KP1 = 79, + KEY_KP2 = 80, + KEY_KP3 = 81, + KEY_KP0 = 82, + KEY_KPDOT = 83, + KEY_ZENKAKUHANKAKU = 85, + KEY_102ND = 86, + KEY_F11 = 87, + KEY_F12 = 88, + KEY_RO = 89, + KEY_KATAKANA = 90, + KEY_HIRAGANA = 91, + KEY_HENKAN = 92, + KEY_KATAKANAHIRAGANA = 93, + KEY_MUHENKAN = 94, + KEY_KPJPCOMMA = 95, + KEY_KPENTER = 96, + KEY_RIGHTCTRL = 97, + KEY_KPSLASH = 98, + KEY_SYSRQ = 99, + KEY_RIGHTALT = 100, + KEY_LINEFEED = 101, + KEY_HOME = 102, + KEY_UP = 103, + KEY_PAGEUP = 104, + KEY_LEFT = 105, + KEY_RIGHT = 106, + KEY_END = 107, + KEY_DOWN = 108, + KEY_PAGEDOWN = 109, + KEY_INSERT = 110, + KEY_DELETE = 111, + KEY_MACRO = 112, + KEY_MUTE = 113, + KEY_VOLUMEDOWN = 114, + KEY_VOLUMEUP = 115, + KEY_POWER = 116, /* SC System Power Down */ + KEY_KPEQUAL = 117, + KEY_KPPLUSMINUS = 118, + KEY_PAUSE = 119, + KEY_SCALE = 120, /* AL Compiz Scale (Expose) */ + KEY_KPCOMMA = 121, + KEY_HANGEUL = 122, + KEY_HANGUEL = KEY_HANGEUL, + KEY_HANJA = 123, + KEY_YEN = 124, + KEY_LEFTMETA = 125, + KEY_RIGHTMETA = 126, + KEY_COMPOSE = 127, + KEY_STOP = 128, /* AC Stop */ + KEY_AGAIN = 129, + KEY_PROPS = 130, /* AC Properties */ + KEY_UNDO = 131, /* AC Undo */ + KEY_FRONT = 132, + KEY_COPY = 133, /* AC Copy */ + KEY_OPEN = 134, /* AC Open */ + KEY_PASTE = 135, /* AC Paste */ + KEY_FIND = 136, /* AC Search */ + KEY_CUT = 137, /* AC Cut */ + KEY_HELP = 138, /* AL Integrated Help Center */ + KEY_MENU = 139, /* Menu (show menu) */ + KEY_CALC = 140, /* AL Calculator */ + KEY_SETUP = 141, + KEY_SLEEP = 142, /* SC System Sleep */ + KEY_WAKEUP = 143, /* System Wake Up */ + KEY_FILE = 144, /* AL Local Machine Browser */ + KEY_SENDFILE = 145, + KEY_DELETEFILE = 146, + KEY_XFER = 147, + KEY_PROG1 = 148, + KEY_PROG2 = 149, + KEY_WWW = 150, /* AL Internet Browser */ + KEY_MSDOS = 151, + KEY_COFFEE = 152, /* AL Terminal Lock/Screensaver */ + KEY_SCREENLOCK = KEY_COFFEE, + KEY_ROTATE_DISPLAY = 153, /* Display orientation for e.g. tablets */ + KEY_DIRECTION = KEY_ROTATE_DISPLAY, + KEY_CYCLEWINDOWS = 154, + KEY_MAIL = 155, + KEY_BOOKMARKS = 156, /* AC Bookmarks */ + KEY_COMPUTER = 157, + KEY_BACK = 158, /* AC Back */ + KEY_FORWARD = 159, /* AC Forward */ + KEY_CLOSECD = 160, + KEY_EJECTCD = 161, + KEY_EJECTCLOSECD = 162, + KEY_NEXTSONG = 163, + KEY_PLAYPAUSE = 164, + KEY_PREVIOUSSONG = 165, + KEY_STOPCD = 166, + KEY_RECORD = 167, + KEY_REWIND = 168, + KEY_PHONE = 169, /* Media Select Telephone */ + KEY_ISO = 170, + KEY_CONFIG = 171, /* AL Consumer Control Configuration */ + KEY_HOMEPAGE = 172, /* AC Home */ + KEY_REFRESH = 173, /* AC Refresh */ + KEY_EXIT = 174, /* AC Exit */ + KEY_MOVE = 175, + KEY_EDIT = 176, + KEY_SCROLLUP = 177, + KEY_SCROLLDOWN = 178, + KEY_KPLEFTPAREN = 179, + KEY_KPRIGHTPAREN = 180, + KEY_NEW = 181, /* AC New */ + KEY_REDO = 182, /* AC Redo/Repeat */ + KEY_F13 = 183, + KEY_F14 = 184, + KEY_F15 = 185, + KEY_F16 = 186, + KEY_F17 = 187, + KEY_F18 = 188, + KEY_F19 = 189, + KEY_F20 = 190, + KEY_F21 = 191, + KEY_F22 = 192, + KEY_F23 = 193, + KEY_F24 = 194, + KEY_PLAYCD = 200, + KEY_PAUSECD = 201, + KEY_PROG3 = 202, + KEY_PROG4 = 203, + KEY_DASHBOARD = 204, /* AL Dashboard */ + KEY_SUSPEND = 205, + KEY_CLOSE = 206, /* AC Close */ + KEY_PLAY = 207, + KEY_FASTFORWARD = 208, + KEY_BASSBOOST = 209, + KEY_PRINT = 210, /* AC Print */ + KEY_HP = 211, + KEY_CAMERA = 212, + KEY_SOUND = 213, + KEY_QUESTION = 214, + KEY_EMAIL = 215, + KEY_CHAT = 216, + KEY_SEARCH = 217, + KEY_CONNECT = 218, + KEY_FINANCE = 219, /* AL Checkbook/Finance */ + KEY_SPORT = 220, + KEY_SHOP = 221, + KEY_ALTERASE = 222, + KEY_CANCEL = 223, /* AC Cancel */ + KEY_BRIGHTNESSDOWN = 224, + KEY_BRIGHTNESSUP = 225, + KEY_MEDIA = 226, + KEY_SWITCHVIDEOMODE = 227, + KEY_KBDILLUMTOGGLE = 228, + KEY_KBDILLUMDOWN = 229, + KEY_KBDILLUMUP = 230, + KEY_SEND = 231, /* AC Send */ + KEY_REPLY = 232, /* AC Reply */ + KEY_FORWARDMAIL = 233, /* AC Forward Msg */ + KEY_SAVE = 234, /* AC Save */ + KEY_DOCUMENTS = 235, + KEY_BATTERY = 236, + KEY_BLUETOOTH = 237, + KEY_WLAN = 238, + KEY_UWB = 239, + KEY_UNKNOWN = 240, + KEY_VIDEO_NEXT = 241, /* drive next video source */ + KEY_VIDEO_PREV = 242, /* drive previous video source */ + KEY_BRIGHTNESS_CYCLE = 243, /* brightness up, after max is min */ + KEY_BRIGHTNESS_AUTO = 244, /* Set Auto Brightness: manual + KEY_BRIGHTNESS_ZERO = KEY_BRIGHTNESS_AUTO, + KEY_DISPLAY_OFF = 245, /* display device to off state */ + KEY_WWAN = 246, /* Wireless WAN (LTE, UMTS, GSM, etc.) */ + KEY_WIMAX = KEY_WWAN, + KEY_RFKILL = 247, /* Key that controls all radios */ + KEY_MICMUTE = 248, /* Mute / unmute the microphone */ + BTN_MISC = 0x100, + BTN_0 = 0x100, + BTN_1 = 0x101, + BTN_2 = 0x102, + BTN_3 = 0x103, + BTN_4 = 0x104, + BTN_5 = 0x105, + BTN_6 = 0x106, + BTN_7 = 0x107, + BTN_8 = 0x108, + BTN_9 = 0x109, + BTN_MOUSE = 0x110, + BTN_LEFT = 0x110, + BTN_RIGHT = 0x111, + BTN_MIDDLE = 0x112, + BTN_SIDE = 0x113, + BTN_EXTRA = 0x114, + BTN_FORWARD = 0x115, + BTN_BACK = 0x116, + BTN_TASK = 0x117, + BTN_JOYSTICK = 0x120, + BTN_TRIGGER = 0x120, + BTN_THUMB = 0x121, + BTN_THUMB2 = 0x122, + BTN_TOP = 0x123, + BTN_TOP2 = 0x124, + BTN_PINKIE = 0x125, + BTN_BASE = 0x126, + BTN_BASE2 = 0x127, + BTN_BASE3 = 0x128, + BTN_BASE4 = 0x129, + BTN_BASE5 = 0x12a, + BTN_BASE6 = 0x12b, + BTN_DEAD = 0x12f, + BTN_GAMEPAD = 0x130, + BTN_SOUTH = 0x130, + BTN_A = BTN_SOUTH, + BTN_EAST = 0x131, + BTN_B = BTN_EAST, + BTN_C = 0x132, + BTN_NORTH = 0x133, + BTN_X = BTN_NORTH, + BTN_WEST = 0x134, + BTN_Y = BTN_WEST, + BTN_Z = 0x135, + BTN_TL = 0x136, + BTN_TR = 0x137, + BTN_TL2 = 0x138, + BTN_TR2 = 0x139, + BTN_SELECT = 0x13a, + BTN_START = 0x13b, + BTN_MODE = 0x13c, + BTN_THUMBL = 0x13d, + BTN_THUMBR = 0x13e, + BTN_DIGI = 0x140, + BTN_TOOL_PEN = 0x140, + BTN_TOOL_RUBBER = 0x141, + BTN_TOOL_BRUSH = 0x142, + BTN_TOOL_PENCIL = 0x143, + BTN_TOOL_AIRBRUSH = 0x144, + BTN_TOOL_FINGER = 0x145, + BTN_TOOL_MOUSE = 0x146, + BTN_TOOL_LENS = 0x147, + BTN_TOOL_QUINTTAP = 0x148, /* Five fingers on trackpad */ + BTN_STYLUS3 = 0x149, + BTN_TOUCH = 0x14a, + BTN_STYLUS = 0x14b, + BTN_STYLUS2 = 0x14c, + BTN_TOOL_DOUBLETAP = 0x14d, + BTN_TOOL_TRIPLETAP = 0x14e, + BTN_TOOL_QUADTAP = 0x14f, /* Four fingers on trackpad */ + BTN_WHEEL = 0x150, + BTN_GEAR_DOWN = 0x150, + BTN_GEAR_UP = 0x151, + KEY_OK = 0x160, + KEY_SELECT = 0x161, + KEY_GOTO = 0x162, + KEY_CLEAR = 0x163, + KEY_POWER2 = 0x164, + KEY_OPTION = 0x165, + KEY_INFO = 0x166, /* AL OEM Features/Tips/Tutorial */ + KEY_TIME = 0x167, + KEY_VENDOR = 0x168, + KEY_ARCHIVE = 0x169, + KEY_PROGRAM = 0x16a, /* Media Select Program Guide */ + KEY_CHANNEL = 0x16b, + KEY_FAVORITES = 0x16c, + KEY_EPG = 0x16d, + KEY_PVR = 0x16e, /* Media Select Home */ + KEY_MHP = 0x16f, + KEY_LANGUAGE = 0x170, + KEY_TITLE = 0x171, + KEY_SUBTITLE = 0x172, + KEY_ANGLE = 0x173, + KEY_FULL_SCREEN = 0x174, /* AC View Toggle */ + KEY_ZOOM = KEY_FULL_SCREEN, + KEY_MODE = 0x175, + KEY_KEYBOARD = 0x176, + KEY_ASPECT_RATIO = 0x177, /* HUTRR37: Aspect */ + KEY_SCREEN = KEY_ASPECT_RATIO, + KEY_PC = 0x178, /* Media Select Computer */ + KEY_TV = 0x179, /* Media Select TV */ + KEY_TV2 = 0x17a, /* Media Select Cable */ + KEY_VCR = 0x17b, /* Media Select VCR */ + KEY_VCR2 = 0x17c, /* VCR Plus */ + KEY_SAT = 0x17d, /* Media Select Satellite */ + KEY_SAT2 = 0x17e, + KEY_CD = 0x17f, /* Media Select CD */ + KEY_TAPE = 0x180, /* Media Select Tape */ + KEY_RADIO = 0x181, + KEY_TUNER = 0x182, /* Media Select Tuner */ + KEY_PLAYER = 0x183, + KEY_TEXT = 0x184, + KEY_DVD = 0x185, /* Media Select DVD */ + KEY_AUX = 0x186, + KEY_MP3 = 0x187, + KEY_AUDIO = 0x188, /* AL Audio Browser */ + KEY_VIDEO = 0x189, /* AL Movie Browser */ + KEY_DIRECTORY = 0x18a, + KEY_LIST = 0x18b, + KEY_MEMO = 0x18c, /* Media Select Messages */ + KEY_CALENDAR = 0x18d, + KEY_RED = 0x18e, + KEY_GREEN = 0x18f, + KEY_YELLOW = 0x190, + KEY_BLUE = 0x191, + KEY_CHANNELUP = 0x192, /* Channel Increment */ + KEY_CHANNELDOWN = 0x193, /* Channel Decrement */ + KEY_FIRST = 0x194, + KEY_LAST = 0x195, /* Recall Last */ + KEY_AB = 0x196, + KEY_NEXT = 0x197, + KEY_RESTART = 0x198, + KEY_SLOW = 0x199, + KEY_SHUFFLE = 0x19a, + KEY_BREAK = 0x19b, + KEY_PREVIOUS = 0x19c, + KEY_DIGITS = 0x19d, + KEY_TEEN = 0x19e, + KEY_TWEN = 0x19f, + KEY_VIDEOPHONE = 0x1a0, /* Media Select Video Phone */ + KEY_GAMES = 0x1a1, /* Media Select Games */ + KEY_ZOOMIN = 0x1a2, /* AC Zoom In */ + KEY_ZOOMOUT = 0x1a3, /* AC Zoom Out */ + KEY_ZOOMRESET = 0x1a4, /* AC Zoom */ + KEY_WORDPROCESSOR = 0x1a5, /* AL Word Processor */ + KEY_EDITOR = 0x1a6, /* AL Text Editor */ + KEY_SPREADSHEET = 0x1a7, /* AL Spreadsheet */ + KEY_GRAPHICSEDITOR = 0x1a8, /* AL Graphics Editor */ + KEY_PRESENTATION = 0x1a9, /* AL Presentation App */ + KEY_DATABASE = 0x1aa, /* AL Database App */ + KEY_NEWS = 0x1ab, /* AL Newsreader */ + KEY_VOICEMAIL = 0x1ac, /* AL Voicemail */ + KEY_ADDRESSBOOK = 0x1ad, /* AL Contacts/Address Book */ + KEY_MESSENGER = 0x1ae, /* AL Instant Messaging */ + KEY_DISPLAYTOGGLE = 0x1af, /* Turn display (LCD) on and off */ + KEY_BRIGHTNESS_TOGGLE = KEY_DISPLAYTOGGLE, + KEY_SPELLCHECK = 0x1b0, /* AL Spell Check */ + KEY_LOGOFF = 0x1b1, /* AL Logoff */ + KEY_DOLLAR = 0x1b2, + KEY_EURO = 0x1b3, + KEY_FRAMEBACK = 0x1b4, /* Consumer - transport controls */ + KEY_FRAMEFORWARD = 0x1b5, + KEY_CONTEXT_MENU = 0x1b6, /* GenDesc - system context menu */ + KEY_MEDIA_REPEAT = 0x1b7, /* Consumer - transport control */ + KEY_10CHANNELSUP = 0x1b8, /* 10 channels up (10+) */ + KEY_10CHANNELSDOWN = 0x1b9, /* 10 channels down (10-) */ + KEY_IMAGES = 0x1ba, /* AL Image Browser */ + KEY_NOTIFICATION_CENTER = 0x1bc, /* Show/hide the notification center */ + KEY_PICKUP_PHONE = 0x1bd, /* Answer incoming call */ + KEY_HANGUP_PHONE = 0x1be, /* Decline incoming call */ + KEY_DEL_EOL = 0x1c0, + KEY_DEL_EOS = 0x1c1, + KEY_INS_LINE = 0x1c2, + KEY_DEL_LINE = 0x1c3, + KEY_FN = 0x1d0, + KEY_FN_ESC = 0x1d1, + KEY_FN_F1 = 0x1d2, + KEY_FN_F2 = 0x1d3, + KEY_FN_F3 = 0x1d4, + KEY_FN_F4 = 0x1d5, + KEY_FN_F5 = 0x1d6, + KEY_FN_F6 = 0x1d7, + KEY_FN_F7 = 0x1d8, + KEY_FN_F8 = 0x1d9, + KEY_FN_F9 = 0x1da, + KEY_FN_F10 = 0x1db, + KEY_FN_F11 = 0x1dc, + KEY_FN_F12 = 0x1dd, + KEY_FN_1 = 0x1de, + KEY_FN_2 = 0x1df, + KEY_FN_D = 0x1e0, + KEY_FN_E = 0x1e1, + KEY_FN_F = 0x1e2, + KEY_FN_S = 0x1e3, + KEY_FN_B = 0x1e4, + KEY_FN_RIGHT_SHIFT = 0x1e5, + KEY_BRL_DOT1 = 0x1f1, + KEY_BRL_DOT2 = 0x1f2, + KEY_BRL_DOT3 = 0x1f3, + KEY_BRL_DOT4 = 0x1f4, + KEY_BRL_DOT5 = 0x1f5, + KEY_BRL_DOT6 = 0x1f6, + KEY_BRL_DOT7 = 0x1f7, + KEY_BRL_DOT8 = 0x1f8, + KEY_BRL_DOT9 = 0x1f9, + KEY_BRL_DOT10 = 0x1fa, + KEY_NUMERIC_0 = 0x200, /* used by phones, remote controls, */ + KEY_NUMERIC_1 = 0x201, /* and other keypads */ + KEY_NUMERIC_2 = 0x202, + KEY_NUMERIC_3 = 0x203, + KEY_NUMERIC_4 = 0x204, + KEY_NUMERIC_5 = 0x205, + KEY_NUMERIC_6 = 0x206, + KEY_NUMERIC_7 = 0x207, + KEY_NUMERIC_8 = 0x208, + KEY_NUMERIC_9 = 0x209, + KEY_NUMERIC_STAR = 0x20a, + KEY_NUMERIC_POUND = 0x20b, + KEY_NUMERIC_A = 0x20c, /* Phone key A - HUT Telephony 0xb9 */ + KEY_NUMERIC_B = 0x20d, + KEY_NUMERIC_C = 0x20e, + KEY_NUMERIC_D = 0x20f, + KEY_CAMERA_FOCUS = 0x210, + KEY_WPS_BUTTON = 0x211, /* WiFi Protected Setup key */ + KEY_TOUCHPAD_TOGGLE = 0x212, /* Request switch touchpad on or off */ + KEY_TOUCHPAD_ON = 0x213, + KEY_TOUCHPAD_OFF = 0x214, + KEY_CAMERA_ZOOMIN = 0x215, + KEY_CAMERA_ZOOMOUT = 0x216, + KEY_CAMERA_UP = 0x217, + KEY_CAMERA_DOWN = 0x218, + KEY_CAMERA_LEFT = 0x219, + KEY_CAMERA_RIGHT = 0x21a, + KEY_ATTENDANT_ON = 0x21b, + KEY_ATTENDANT_OFF = 0x21c, + KEY_ATTENDANT_TOGGLE = 0x21d, /* Attendant call on or off */ + KEY_LIGHTS_TOGGLE = 0x21e, /* Reading light on or off */ + BTN_DPAD_UP = 0x220, + BTN_DPAD_DOWN = 0x221, + BTN_DPAD_LEFT = 0x222, + BTN_DPAD_RIGHT = 0x223, + KEY_ALS_TOGGLE = 0x230, /* Ambient light sensor */ + KEY_ROTATE_LOCK_TOGGLE = 0x231, /* Display rotation lock */ + KEY_BUTTONCONFIG = 0x240, /* AL Button Configuration */ + KEY_TASKMANAGER = 0x241, /* AL Task/Project Manager */ + KEY_JOURNAL = 0x242, /* AL Log/Journal/Timecard */ + KEY_CONTROLPANEL = 0x243, /* AL Control Panel */ + KEY_APPSELECT = 0x244, /* AL Select Task/Application */ + KEY_SCREENSAVER = 0x245, /* AL Screen Saver */ + KEY_VOICECOMMAND = 0x246, /* Listening Voice Command */ + KEY_ASSISTANT = 0x247, /* AL Context-aware desktop assistant */ + KEY_KBD_LAYOUT_NEXT = 0x248, /* AC Next Keyboard Layout Select */ + KEY_EMOJI_PICKER = 0x249, /* Show/hide emoji picker (HUTRR101) */ + KEY_BRIGHTNESS_MIN = 0x250, /* Set Brightness to Minimum */ + KEY_BRIGHTNESS_MAX = 0x251, /* Set Brightness to Maximum */ + KEY_KBDINPUTASSIST_PREV = 0x260, + KEY_KBDINPUTASSIST_NEXT = 0x261, + KEY_KBDINPUTASSIST_PREVGROUP = 0x262, + KEY_KBDINPUTASSIST_NEXTGROUP = 0x263, + KEY_KBDINPUTASSIST_ACCEPT = 0x264, + KEY_KBDINPUTASSIST_CANCEL = 0x265, + KEY_RIGHT_UP = 0x266, + KEY_RIGHT_DOWN = 0x267, + KEY_LEFT_UP = 0x268, + KEY_LEFT_DOWN = 0x269, + KEY_ROOT_MENU = 0x26a, /* Show Device's Root Menu */ + KEY_MEDIA_TOP_MENU = 0x26b, + KEY_NUMERIC_11 = 0x26c, + KEY_NUMERIC_12 = 0x26d, + KEY_AUDIO_DESC = 0x26e, + KEY_3D_MODE = 0x26f, + KEY_NEXT_FAVORITE = 0x270, + KEY_STOP_RECORD = 0x271, + KEY_PAUSE_RECORD = 0x272, + KEY_VOD = 0x273, /* Video on Demand */ + KEY_UNMUTE = 0x274, + KEY_FASTREVERSE = 0x275, + KEY_SLOWREVERSE = 0x276, + KEY_DATA = 0x277, + KEY_ONSCREEN_KEYBOARD = 0x278, + KEY_PRIVACY_SCREEN_TOGGLE = 0x279, + KEY_SELECTIVE_SCREENSHOT = 0x27a, + KEY_MACRO1 = 0x290, + KEY_MACRO2 = 0x291, + KEY_MACRO3 = 0x292, + KEY_MACRO4 = 0x293, + KEY_MACRO5 = 0x294, + KEY_MACRO6 = 0x295, + KEY_MACRO7 = 0x296, + KEY_MACRO8 = 0x297, + KEY_MACRO9 = 0x298, + KEY_MACRO10 = 0x299, + KEY_MACRO11 = 0x29a, + KEY_MACRO12 = 0x29b, + KEY_MACRO13 = 0x29c, + KEY_MACRO14 = 0x29d, + KEY_MACRO15 = 0x29e, + KEY_MACRO16 = 0x29f, + KEY_MACRO17 = 0x2a0, + KEY_MACRO18 = 0x2a1, + KEY_MACRO19 = 0x2a2, + KEY_MACRO20 = 0x2a3, + KEY_MACRO21 = 0x2a4, + KEY_MACRO22 = 0x2a5, + KEY_MACRO23 = 0x2a6, + KEY_MACRO24 = 0x2a7, + KEY_MACRO25 = 0x2a8, + KEY_MACRO26 = 0x2a9, + KEY_MACRO27 = 0x2aa, + KEY_MACRO28 = 0x2ab, + KEY_MACRO29 = 0x2ac, + KEY_MACRO30 = 0x2ad, + KEY_MACRO_RECORD_START = 0x2b0, + KEY_MACRO_RECORD_STOP = 0x2b1, + KEY_MACRO_PRESET_CYCLE = 0x2b2, + KEY_MACRO_PRESET1 = 0x2b3, + KEY_MACRO_PRESET2 = 0x2b4, + KEY_MACRO_PRESET3 = 0x2b5, + KEY_KBD_LCD_MENU1 = 0x2b8, + KEY_KBD_LCD_MENU2 = 0x2b9, + KEY_KBD_LCD_MENU3 = 0x2ba, + KEY_KBD_LCD_MENU4 = 0x2bb, + KEY_KBD_LCD_MENU5 = 0x2bc, + BTN_TRIGGER_HAPPY = 0x2c0, + BTN_TRIGGER_HAPPY1 = 0x2c0, + BTN_TRIGGER_HAPPY2 = 0x2c1, + BTN_TRIGGER_HAPPY3 = 0x2c2, + BTN_TRIGGER_HAPPY4 = 0x2c3, + BTN_TRIGGER_HAPPY5 = 0x2c4, + BTN_TRIGGER_HAPPY6 = 0x2c5, + BTN_TRIGGER_HAPPY7 = 0x2c6, + BTN_TRIGGER_HAPPY8 = 0x2c7, + BTN_TRIGGER_HAPPY9 = 0x2c8, + BTN_TRIGGER_HAPPY10 = 0x2c9, + BTN_TRIGGER_HAPPY11 = 0x2ca, + BTN_TRIGGER_HAPPY12 = 0x2cb, + BTN_TRIGGER_HAPPY13 = 0x2cc, + BTN_TRIGGER_HAPPY14 = 0x2cd, + BTN_TRIGGER_HAPPY15 = 0x2ce, + BTN_TRIGGER_HAPPY16 = 0x2cf, + BTN_TRIGGER_HAPPY17 = 0x2d0, + BTN_TRIGGER_HAPPY18 = 0x2d1, + BTN_TRIGGER_HAPPY19 = 0x2d2, + BTN_TRIGGER_HAPPY20 = 0x2d3, + BTN_TRIGGER_HAPPY21 = 0x2d4, + BTN_TRIGGER_HAPPY22 = 0x2d5, + BTN_TRIGGER_HAPPY23 = 0x2d6, + BTN_TRIGGER_HAPPY24 = 0x2d7, + BTN_TRIGGER_HAPPY25 = 0x2d8, + BTN_TRIGGER_HAPPY26 = 0x2d9, + BTN_TRIGGER_HAPPY27 = 0x2da, + BTN_TRIGGER_HAPPY28 = 0x2db, + BTN_TRIGGER_HAPPY29 = 0x2dc, + BTN_TRIGGER_HAPPY30 = 0x2dd, + BTN_TRIGGER_HAPPY31 = 0x2de, + BTN_TRIGGER_HAPPY32 = 0x2df, + BTN_TRIGGER_HAPPY33 = 0x2e0, + BTN_TRIGGER_HAPPY34 = 0x2e1, + BTN_TRIGGER_HAPPY35 = 0x2e2, + BTN_TRIGGER_HAPPY36 = 0x2e3, + BTN_TRIGGER_HAPPY37 = 0x2e4, + BTN_TRIGGER_HAPPY38 = 0x2e5, + BTN_TRIGGER_HAPPY39 = 0x2e6, + BTN_TRIGGER_HAPPY40 = 0x2e7, + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxRelativeAxis.cs b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxRelativeAxis.cs new file mode 100644 index 000000000..c209bb920 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Providers/Input/LinuxRelativeAxis.cs @@ -0,0 +1,16 @@ +namespace Artemis.UI.Linux.Providers.Input +{ + public enum LinuxRelativeAxis + { + REL_X = 0x00, + REL_Y = 0x01, + REL_Z = 0x02, + REL_RX = 0x03, + REL_RY = 0x04, + REL_RZ = 0x05, + REL_HWHEEL = 0x06, + REL_DIAL = 0x07, + REL_WHEEL = 0x08, + REL_MISC = 0x09, + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Linux/Utilities/InputUtilities.cs b/src/Avalonia/Artemis.UI.Linux/Utilities/InputUtilities.cs new file mode 100644 index 000000000..3fd33fbcb --- /dev/null +++ b/src/Avalonia/Artemis.UI.Linux/Utilities/InputUtilities.cs @@ -0,0 +1,621 @@ +using System; +using Artemis.Core.Services; +using Artemis.UI.Linux.Providers.Input; + +namespace Artemis.UI.Linux.Utilities +{ + public static class InputUtilities + { + public static KeyboardKey KeyFromKeyCode(LinuxKeyboardKeyCodes code) => code switch + { + LinuxKeyboardKeyCodes.KEY_ESC => KeyboardKey.Escape, + LinuxKeyboardKeyCodes.KEY_1 => KeyboardKey.D1, + LinuxKeyboardKeyCodes.KEY_2 => KeyboardKey.D2, + LinuxKeyboardKeyCodes.KEY_3 => KeyboardKey.D3, + LinuxKeyboardKeyCodes.KEY_4 => KeyboardKey.D4, + LinuxKeyboardKeyCodes.KEY_5 => KeyboardKey.D5, + LinuxKeyboardKeyCodes.KEY_6 => KeyboardKey.D6, + LinuxKeyboardKeyCodes.KEY_7 => KeyboardKey.D7, + LinuxKeyboardKeyCodes.KEY_8 => KeyboardKey.D8, + LinuxKeyboardKeyCodes.KEY_9 => KeyboardKey.D9, + LinuxKeyboardKeyCodes.KEY_0 => KeyboardKey.D0, + LinuxKeyboardKeyCodes.KEY_MINUS => KeyboardKey.OemMinus, + LinuxKeyboardKeyCodes.KEY_EQUAL => KeyboardKey.OemPlus, + LinuxKeyboardKeyCodes.KEY_BACKSPACE => KeyboardKey.Back, + LinuxKeyboardKeyCodes.KEY_TAB => KeyboardKey.Tab, + LinuxKeyboardKeyCodes.KEY_Q => KeyboardKey.Q, + LinuxKeyboardKeyCodes.KEY_W => KeyboardKey.W, + LinuxKeyboardKeyCodes.KEY_E => KeyboardKey.E, + LinuxKeyboardKeyCodes.KEY_R => KeyboardKey.R, + LinuxKeyboardKeyCodes.KEY_T => KeyboardKey.T, + LinuxKeyboardKeyCodes.KEY_Y => KeyboardKey.Y, + LinuxKeyboardKeyCodes.KEY_U => KeyboardKey.U, + LinuxKeyboardKeyCodes.KEY_I => KeyboardKey.I, + LinuxKeyboardKeyCodes.KEY_O => KeyboardKey.O, + LinuxKeyboardKeyCodes.KEY_P => KeyboardKey.P, + LinuxKeyboardKeyCodes.KEY_LEFTBRACE => KeyboardKey.OemOpenBrackets, + LinuxKeyboardKeyCodes.KEY_RIGHTBRACE => KeyboardKey.OemCloseBrackets, + LinuxKeyboardKeyCodes.KEY_ENTER => KeyboardKey.Enter, + LinuxKeyboardKeyCodes.KEY_LEFTCTRL => KeyboardKey.LeftCtrl, + LinuxKeyboardKeyCodes.KEY_A => KeyboardKey.A, + LinuxKeyboardKeyCodes.KEY_S => KeyboardKey.S, + LinuxKeyboardKeyCodes.KEY_D => KeyboardKey.D, + LinuxKeyboardKeyCodes.KEY_F => KeyboardKey.F, + LinuxKeyboardKeyCodes.KEY_G => KeyboardKey.G, + LinuxKeyboardKeyCodes.KEY_H => KeyboardKey.H, + LinuxKeyboardKeyCodes.KEY_J => KeyboardKey.J, + LinuxKeyboardKeyCodes.KEY_K => KeyboardKey.K, + LinuxKeyboardKeyCodes.KEY_L => KeyboardKey.L, + LinuxKeyboardKeyCodes.KEY_SEMICOLON => KeyboardKey.OemSemicolon, + LinuxKeyboardKeyCodes.KEY_APOSTROPHE => KeyboardKey.OemQuotes, + LinuxKeyboardKeyCodes.KEY_GRAVE => KeyboardKey.OemTilde, + LinuxKeyboardKeyCodes.KEY_LEFTSHIFT => KeyboardKey.LeftShift, + LinuxKeyboardKeyCodes.KEY_BACKSLASH => KeyboardKey.OemBackslash, + LinuxKeyboardKeyCodes.KEY_Z => KeyboardKey.Z, + LinuxKeyboardKeyCodes.KEY_X => KeyboardKey.X, + LinuxKeyboardKeyCodes.KEY_C => KeyboardKey.C, + LinuxKeyboardKeyCodes.KEY_V => KeyboardKey.V, + LinuxKeyboardKeyCodes.KEY_B => KeyboardKey.B, + LinuxKeyboardKeyCodes.KEY_N => KeyboardKey.N, + LinuxKeyboardKeyCodes.KEY_M => KeyboardKey.M, + LinuxKeyboardKeyCodes.KEY_COMMA => KeyboardKey.OemComma, + LinuxKeyboardKeyCodes.KEY_DOT => KeyboardKey.OemPeriod, + LinuxKeyboardKeyCodes.KEY_SLASH => KeyboardKey.OemQuestion, + LinuxKeyboardKeyCodes.KEY_RIGHTSHIFT => KeyboardKey.RightShift, + //LinuxKeyboardKeyCodes.KEY_KPASTERISK => , + LinuxKeyboardKeyCodes.KEY_LEFTALT => KeyboardKey.LeftAlt, + LinuxKeyboardKeyCodes.KEY_SPACE => KeyboardKey.Space, + LinuxKeyboardKeyCodes.KEY_CAPSLOCK => KeyboardKey.CapsLock, + LinuxKeyboardKeyCodes.KEY_F1 => KeyboardKey.F1, + LinuxKeyboardKeyCodes.KEY_F2 => KeyboardKey.F2, + LinuxKeyboardKeyCodes.KEY_F3 => KeyboardKey.F3, + LinuxKeyboardKeyCodes.KEY_F4 => KeyboardKey.F4, + LinuxKeyboardKeyCodes.KEY_F5 => KeyboardKey.F5, + LinuxKeyboardKeyCodes.KEY_F6 => KeyboardKey.F6, + LinuxKeyboardKeyCodes.KEY_F7 => KeyboardKey.F7, + LinuxKeyboardKeyCodes.KEY_F8 => KeyboardKey.F8, + LinuxKeyboardKeyCodes.KEY_F9 => KeyboardKey.F9, + LinuxKeyboardKeyCodes.KEY_F10 => KeyboardKey.F10, + LinuxKeyboardKeyCodes.KEY_NUMLOCK => KeyboardKey.NumLock, + LinuxKeyboardKeyCodes.KEY_SCROLLLOCK => KeyboardKey.Scroll, + LinuxKeyboardKeyCodes.KEY_KP7 => KeyboardKey.NumPad7, + LinuxKeyboardKeyCodes.KEY_KP8 => KeyboardKey.NumPad8, + LinuxKeyboardKeyCodes.KEY_KP9 => KeyboardKey.NumPad9, + LinuxKeyboardKeyCodes.KEY_KPMINUS => KeyboardKey.Subtract, + LinuxKeyboardKeyCodes.KEY_KP4 => KeyboardKey.NumPad4, + LinuxKeyboardKeyCodes.KEY_KP5 => KeyboardKey.NumPad5, + LinuxKeyboardKeyCodes.KEY_KP6 => KeyboardKey.NumPad6, + LinuxKeyboardKeyCodes.KEY_KPPLUS => KeyboardKey.Add, + LinuxKeyboardKeyCodes.KEY_KP1 => KeyboardKey.NumPad1, + LinuxKeyboardKeyCodes.KEY_KP2 => KeyboardKey.NumPad2, + LinuxKeyboardKeyCodes.KEY_KP3 => KeyboardKey.NumPad3, + LinuxKeyboardKeyCodes.KEY_KP0 => KeyboardKey.NumPad0, + LinuxKeyboardKeyCodes.KEY_KPDOT => KeyboardKey.Decimal, + // LinuxKeyboardKeyCodes.KEY_ZENKAKUHANKAKU => expr, + // LinuxKeyboardKeyCodes.KEY_102ND => expr, + LinuxKeyboardKeyCodes.KEY_F11 => KeyboardKey.F11, + LinuxKeyboardKeyCodes.KEY_F12 => KeyboardKey.F12, + //LinuxKeyboardKeyCodes.KEY_RO => expr, + // LinuxKeyboardKeyCodes.KEY_KATAKANA => expr, + // LinuxKeyboardKeyCodes.KEY_HIRAGANA => expr, + // LinuxKeyboardKeyCodes.KEY_HENKAN => expr, + // LinuxKeyboardKeyCodes.KEY_KATAKANAHIRAGANA => expr, + // LinuxKeyboardKeyCodes.KEY_MUHENKAN => expr, + // LinuxKeyboardKeyCodes.KEY_KPJPCOMMA => expr, + LinuxKeyboardKeyCodes.KEY_KPENTER => KeyboardKey.NumPadEnter, + LinuxKeyboardKeyCodes.KEY_RIGHTCTRL => KeyboardKey.RightCtrl, + LinuxKeyboardKeyCodes.KEY_KPSLASH => KeyboardKey.Divide, + LinuxKeyboardKeyCodes.KEY_SYSRQ => KeyboardKey.System,//TODO:? + LinuxKeyboardKeyCodes.KEY_RIGHTALT => KeyboardKey.RightAlt, + LinuxKeyboardKeyCodes.KEY_LINEFEED => KeyboardKey.LineFeed, + LinuxKeyboardKeyCodes.KEY_HOME => KeyboardKey.Home, + LinuxKeyboardKeyCodes.KEY_UP => KeyboardKey.Up, + LinuxKeyboardKeyCodes.KEY_PAGEUP => KeyboardKey.PageUp, + LinuxKeyboardKeyCodes.KEY_LEFT => KeyboardKey.Left, + LinuxKeyboardKeyCodes.KEY_RIGHT => KeyboardKey.Right, + LinuxKeyboardKeyCodes.KEY_END => KeyboardKey.End, + LinuxKeyboardKeyCodes.KEY_DOWN => KeyboardKey.Down, + LinuxKeyboardKeyCodes.KEY_PAGEDOWN => KeyboardKey.PageDown, + LinuxKeyboardKeyCodes.KEY_INSERT => KeyboardKey.Insert, + LinuxKeyboardKeyCodes.KEY_DELETE => KeyboardKey.Delete, + // LinuxKeyboardKeyCodes.KEY_MACRO => KeyboardKey.macro, + LinuxKeyboardKeyCodes.KEY_MUTE => KeyboardKey.VolumeMute, + LinuxKeyboardKeyCodes.KEY_VOLUMEDOWN => KeyboardKey.VolumeDown, + LinuxKeyboardKeyCodes.KEY_VOLUMEUP => KeyboardKey.VolumeUp, + // LinuxKeyboardKeyCodes.KEY_POWER => KeyboardKey.power, + LinuxKeyboardKeyCodes.KEY_KPEQUAL => KeyboardKey.NumPadEnter,//todo: ? + // LinuxKeyboardKeyCodes.KEY_KPPLUSMINUS => KeyboardKey.numpad, + LinuxKeyboardKeyCodes.KEY_PAUSE => KeyboardKey.Pause, + // LinuxKeyboardKeyCodes.KEY_SCALE => KeyboardKey.scal, + // LinuxKeyboardKeyCodes.KEY_KPCOMMA => KeyboardKey.ke, + // LinuxKeyboardKeyCodes.KEY_HANGEUL => expr, + // LinuxKeyboardKeyCodes.KEY_HANJA => expr, + // LinuxKeyboardKeyCodes.KEY_YEN => expr, + LinuxKeyboardKeyCodes.KEY_LEFTMETA => KeyboardKey.LWin, + LinuxKeyboardKeyCodes.KEY_RIGHTMETA => KeyboardKey.RWin, + // LinuxKeyboardKeyCodes.KEY_COMPOSE => , + LinuxKeyboardKeyCodes.KEY_STOP => KeyboardKey.MediaStop, + // LinuxKeyboardKeyCodes.KEY_AGAIN => , + // LinuxKeyboardKeyCodes.KEY_PROPS => expr, + // LinuxKeyboardKeyCodes.KEY_UNDO => expr, + // LinuxKeyboardKeyCodes.KEY_FRONT => expr, + // LinuxKeyboardKeyCodes.KEY_COPY => expr, + // LinuxKeyboardKeyCodes.KEY_OPEN => expr, + // LinuxKeyboardKeyCodes.KEY_PASTE => expr, + // LinuxKeyboardKeyCodes.KEY_FIND => expr, + // LinuxKeyboardKeyCodes.KEY_CUT => expr, + // LinuxKeyboardKeyCodes.KEY_HELP => expr, + // LinuxKeyboardKeyCodes.KEY_MENU => expr, + // LinuxKeyboardKeyCodes.KEY_CALC => expr, + // LinuxKeyboardKeyCodes.KEY_SETUP => expr, + // LinuxKeyboardKeyCodes.KEY_SLEEP => expr, + // LinuxKeyboardKeyCodes.KEY_WAKEUP => expr, + // LinuxKeyboardKeyCodes.KEY_FILE => expr, + // LinuxKeyboardKeyCodes.KEY_SENDFILE => expr, + // LinuxKeyboardKeyCodes.KEY_DELETEFILE => expr, + // LinuxKeyboardKeyCodes.KEY_XFER => expr, + // LinuxKeyboardKeyCodes.KEY_PROG1 => expr, + // LinuxKeyboardKeyCodes.KEY_PROG2 => expr, + // LinuxKeyboardKeyCodes.KEY_WWW => expr, + // LinuxKeyboardKeyCodes.KEY_MSDOS => expr, + // LinuxKeyboardKeyCodes.KEY_COFFEE => expr, + // LinuxKeyboardKeyCodes.KEY_ROTATE_DISPLAY => expr, + // LinuxKeyboardKeyCodes.KEY_CYCLEWINDOWS => expr, + // LinuxKeyboardKeyCodes.KEY_MAIL => expr, + // LinuxKeyboardKeyCodes.KEY_BOOKMARKS => expr, + // LinuxKeyboardKeyCodes.KEY_COMPUTER => expr, + // LinuxKeyboardKeyCodes.KEY_BACK => expr, + // LinuxKeyboardKeyCodes.KEY_FORWARD => expr, + // LinuxKeyboardKeyCodes.KEY_CLOSECD => expr, + // LinuxKeyboardKeyCodes.KEY_EJECTCD => expr, + // LinuxKeyboardKeyCodes.KEY_EJECTCLOSECD => expr, + // LinuxKeyboardKeyCodes.KEY_NEXTSONG => expr, + // LinuxKeyboardKeyCodes.KEY_PLAYPAUSE => expr, + // LinuxKeyboardKeyCodes.KEY_PREVIOUSSONG => expr, + // LinuxKeyboardKeyCodes.KEY_STOPCD => expr, + // LinuxKeyboardKeyCodes.KEY_RECORD => expr, + // LinuxKeyboardKeyCodes.KEY_REWIND => expr, + // LinuxKeyboardKeyCodes.KEY_PHONE => expr, + // LinuxKeyboardKeyCodes.KEY_ISO => expr, + // LinuxKeyboardKeyCodes.KEY_CONFIG => expr, + // LinuxKeyboardKeyCodes.KEY_HOMEPAGE => expr, + // LinuxKeyboardKeyCodes.KEY_REFRESH => expr, + // LinuxKeyboardKeyCodes.KEY_EXIT => expr, + // LinuxKeyboardKeyCodes.KEY_MOVE => expr, + // LinuxKeyboardKeyCodes.KEY_EDIT => expr, + // LinuxKeyboardKeyCodes.KEY_SCROLLUP => expr, + // LinuxKeyboardKeyCodes.KEY_SCROLLDOWN => expr, + // LinuxKeyboardKeyCodes.KEY_KPLEFTPAREN => expr, + // LinuxKeyboardKeyCodes.KEY_KPRIGHTPAREN => expr, + // LinuxKeyboardKeyCodes.KEY_NEW => expr, + // LinuxKeyboardKeyCodes.KEY_REDO => expr, + // LinuxKeyboardKeyCodes.KEY_F13 => expr, + // LinuxKeyboardKeyCodes.KEY_F14 => expr, + // LinuxKeyboardKeyCodes.KEY_F15 => expr, + // LinuxKeyboardKeyCodes.KEY_F16 => expr, + // LinuxKeyboardKeyCodes.KEY_F17 => expr, + // LinuxKeyboardKeyCodes.KEY_F18 => expr, + // LinuxKeyboardKeyCodes.KEY_F19 => expr, + // LinuxKeyboardKeyCodes.KEY_F20 => expr, + // LinuxKeyboardKeyCodes.KEY_F21 => expr, + // LinuxKeyboardKeyCodes.KEY_F22 => expr, + // LinuxKeyboardKeyCodes.KEY_F23 => expr, + // LinuxKeyboardKeyCodes.KEY_F24 => expr, + // LinuxKeyboardKeyCodes.KEY_PLAYCD => expr, + // LinuxKeyboardKeyCodes.KEY_PAUSECD => expr, + // LinuxKeyboardKeyCodes.KEY_PROG3 => expr, + // LinuxKeyboardKeyCodes.KEY_PROG4 => expr, + // LinuxKeyboardKeyCodes.KEY_DASHBOARD => expr, + // LinuxKeyboardKeyCodes.KEY_SUSPEND => expr, + // LinuxKeyboardKeyCodes.KEY_CLOSE => expr, + // LinuxKeyboardKeyCodes.KEY_PLAY => expr, + // LinuxKeyboardKeyCodes.KEY_FASTFORWARD => expr, + // LinuxKeyboardKeyCodes.KEY_BASSBOOST => expr, + // LinuxKeyboardKeyCodes.KEY_PRINT => expr, + // LinuxKeyboardKeyCodes.KEY_HP => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA => expr, + // LinuxKeyboardKeyCodes.KEY_SOUND => expr, + // LinuxKeyboardKeyCodes.KEY_QUESTION => expr, + // LinuxKeyboardKeyCodes.KEY_EMAIL => expr, + // LinuxKeyboardKeyCodes.KEY_CHAT => expr, + // LinuxKeyboardKeyCodes.KEY_SEARCH => expr, + // LinuxKeyboardKeyCodes.KEY_CONNECT => expr, + // LinuxKeyboardKeyCodes.KEY_FINANCE => expr, + // LinuxKeyboardKeyCodes.KEY_SPORT => expr, + // LinuxKeyboardKeyCodes.KEY_SHOP => expr, + // LinuxKeyboardKeyCodes.KEY_ALTERASE => expr, + // LinuxKeyboardKeyCodes.KEY_CANCEL => expr, + // LinuxKeyboardKeyCodes.KEY_BRIGHTNESSDOWN => expr, + // LinuxKeyboardKeyCodes.KEY_BRIGHTNESSUP => expr, + // LinuxKeyboardKeyCodes.KEY_MEDIA => expr, + // LinuxKeyboardKeyCodes.KEY_SWITCHVIDEOMODE => expr, + // LinuxKeyboardKeyCodes.KEY_KBDILLUMTOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_KBDILLUMDOWN => expr, + // LinuxKeyboardKeyCodes.KEY_KBDILLUMUP => expr, + // LinuxKeyboardKeyCodes.KEY_SEND => expr, + // LinuxKeyboardKeyCodes.KEY_REPLY => expr, + // LinuxKeyboardKeyCodes.KEY_FORWARDMAIL => expr, + // LinuxKeyboardKeyCodes.KEY_SAVE => expr, + // LinuxKeyboardKeyCodes.KEY_DOCUMENTS => expr, + // LinuxKeyboardKeyCodes.KEY_BATTERY => expr, + // LinuxKeyboardKeyCodes.KEY_BLUETOOTH => expr, + // LinuxKeyboardKeyCodes.KEY_WLAN => expr, + // LinuxKeyboardKeyCodes.KEY_UWB => expr, + // LinuxKeyboardKeyCodes.KEY_UNKNOWN => expr, + // LinuxKeyboardKeyCodes.KEY_VIDEO_NEXT => expr, + // LinuxKeyboardKeyCodes.KEY_VIDEO_PREV => expr, + // LinuxKeyboardKeyCodes.KEY_BRIGHTNESS_CYCLE => expr, + // LinuxKeyboardKeyCodes.KEY_BRIGHTNESS_AUTO => expr, + // LinuxKeyboardKeyCodes.KEY_WWAN => expr, + // LinuxKeyboardKeyCodes.KEY_RFKILL => expr, + // LinuxKeyboardKeyCodes.KEY_MICMUTE => expr, + // LinuxKeyboardKeyCodes.BTN_MISC => expr, + // LinuxKeyboardKeyCodes.BTN_1 => expr, + // LinuxKeyboardKeyCodes.BTN_2 => expr, + // LinuxKeyboardKeyCodes.BTN_3 => expr, + // LinuxKeyboardKeyCodes.BTN_4 => expr, + // LinuxKeyboardKeyCodes.BTN_5 => expr, + // LinuxKeyboardKeyCodes.BTN_6 => expr, + // LinuxKeyboardKeyCodes.BTN_7 => expr, + // LinuxKeyboardKeyCodes.BTN_8 => expr, + // LinuxKeyboardKeyCodes.BTN_9 => expr, + // LinuxKeyboardKeyCodes.BTN_MOUSE => expr, + // LinuxKeyboardKeyCodes.BTN_RIGHT => expr, + // LinuxKeyboardKeyCodes.BTN_MIDDLE => expr, + // LinuxKeyboardKeyCodes.BTN_SIDE => expr, + // LinuxKeyboardKeyCodes.BTN_EXTRA => expr, + // LinuxKeyboardKeyCodes.BTN_FORWARD => expr, + // LinuxKeyboardKeyCodes.BTN_BACK => expr, + // LinuxKeyboardKeyCodes.BTN_TASK => expr, + // LinuxKeyboardKeyCodes.BTN_JOYSTICK => expr, + // LinuxKeyboardKeyCodes.BTN_THUMB => expr, + // LinuxKeyboardKeyCodes.BTN_THUMB2 => expr, + // LinuxKeyboardKeyCodes.BTN_TOP => expr, + // LinuxKeyboardKeyCodes.BTN_TOP2 => expr, + // LinuxKeyboardKeyCodes.BTN_PINKIE => expr, + // LinuxKeyboardKeyCodes.BTN_BASE => expr, + // LinuxKeyboardKeyCodes.BTN_BASE2 => expr, + // LinuxKeyboardKeyCodes.BTN_BASE3 => expr, + // LinuxKeyboardKeyCodes.BTN_BASE4 => expr, + // LinuxKeyboardKeyCodes.BTN_BASE5 => expr, + // LinuxKeyboardKeyCodes.BTN_BASE6 => expr, + // LinuxKeyboardKeyCodes.BTN_DEAD => expr, + // LinuxKeyboardKeyCodes.BTN_GAMEPAD => expr, + // LinuxKeyboardKeyCodes.BTN_EAST => expr, + // LinuxKeyboardKeyCodes.BTN_C => expr, + // LinuxKeyboardKeyCodes.BTN_NORTH => expr, + // LinuxKeyboardKeyCodes.BTN_WEST => expr, + // LinuxKeyboardKeyCodes.BTN_Z => expr, + // LinuxKeyboardKeyCodes.BTN_TL => expr, + // LinuxKeyboardKeyCodes.BTN_TR => expr, + // LinuxKeyboardKeyCodes.BTN_TL2 => expr, + // LinuxKeyboardKeyCodes.BTN_TR2 => expr, + // LinuxKeyboardKeyCodes.BTN_SELECT => expr, + // LinuxKeyboardKeyCodes.BTN_START => expr, + // LinuxKeyboardKeyCodes.BTN_MODE => expr, + // LinuxKeyboardKeyCodes.BTN_THUMBL => expr, + // LinuxKeyboardKeyCodes.BTN_THUMBR => expr, + // LinuxKeyboardKeyCodes.BTN_DIGI => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_RUBBER => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_BRUSH => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_PENCIL => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_AIRBRUSH => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_FINGER => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_MOUSE => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_LENS => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_QUINTTAP => expr, + // LinuxKeyboardKeyCodes.BTN_STYLUS3 => expr, + // LinuxKeyboardKeyCodes.BTN_TOUCH => expr, + // LinuxKeyboardKeyCodes.BTN_STYLUS => expr, + // LinuxKeyboardKeyCodes.BTN_STYLUS2 => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_DOUBLETAP => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_TRIPLETAP => expr, + // LinuxKeyboardKeyCodes.BTN_TOOL_QUADTAP => expr, + // LinuxKeyboardKeyCodes.BTN_WHEEL => expr, + // LinuxKeyboardKeyCodes.BTN_GEAR_UP => expr, + // LinuxKeyboardKeyCodes.KEY_OK => expr, + // LinuxKeyboardKeyCodes.KEY_SELECT => expr, + // LinuxKeyboardKeyCodes.KEY_GOTO => expr, + // LinuxKeyboardKeyCodes.KEY_CLEAR => expr, + // LinuxKeyboardKeyCodes.KEY_POWER2 => expr, + // LinuxKeyboardKeyCodes.KEY_OPTION => expr, + // LinuxKeyboardKeyCodes.KEY_INFO => expr, + // LinuxKeyboardKeyCodes.KEY_TIME => expr, + // LinuxKeyboardKeyCodes.KEY_VENDOR => expr, + // LinuxKeyboardKeyCodes.KEY_ARCHIVE => expr, + // LinuxKeyboardKeyCodes.KEY_PROGRAM => expr, + // LinuxKeyboardKeyCodes.KEY_CHANNEL => expr, + // LinuxKeyboardKeyCodes.KEY_FAVORITES => expr, + // LinuxKeyboardKeyCodes.KEY_EPG => expr, + // LinuxKeyboardKeyCodes.KEY_PVR => expr, + // LinuxKeyboardKeyCodes.KEY_MHP => expr, + // LinuxKeyboardKeyCodes.KEY_LANGUAGE => expr, + // LinuxKeyboardKeyCodes.KEY_TITLE => expr, + // LinuxKeyboardKeyCodes.KEY_SUBTITLE => expr, + // LinuxKeyboardKeyCodes.KEY_ANGLE => expr, + // LinuxKeyboardKeyCodes.KEY_FULL_SCREEN => expr, + // LinuxKeyboardKeyCodes.KEY_MODE => expr, + // LinuxKeyboardKeyCodes.KEY_KEYBOARD => expr, + // LinuxKeyboardKeyCodes.KEY_ASPECT_RATIO => expr, + // LinuxKeyboardKeyCodes.KEY_PC => expr, + // LinuxKeyboardKeyCodes.KEY_TV => expr, + // LinuxKeyboardKeyCodes.KEY_TV2 => expr, + // LinuxKeyboardKeyCodes.KEY_VCR => expr, + // LinuxKeyboardKeyCodes.KEY_VCR2 => expr, + // LinuxKeyboardKeyCodes.KEY_SAT => expr, + // LinuxKeyboardKeyCodes.KEY_SAT2 => expr, + // LinuxKeyboardKeyCodes.KEY_CD => expr, + // LinuxKeyboardKeyCodes.KEY_TAPE => expr, + // LinuxKeyboardKeyCodes.KEY_RADIO => expr, + // LinuxKeyboardKeyCodes.KEY_TUNER => expr, + // LinuxKeyboardKeyCodes.KEY_PLAYER => expr, + // LinuxKeyboardKeyCodes.KEY_TEXT => expr, + // LinuxKeyboardKeyCodes.KEY_DVD => expr, + // LinuxKeyboardKeyCodes.KEY_AUX => expr, + // LinuxKeyboardKeyCodes.KEY_MP3 => expr, + // LinuxKeyboardKeyCodes.KEY_AUDIO => expr, + // LinuxKeyboardKeyCodes.KEY_VIDEO => expr, + // LinuxKeyboardKeyCodes.KEY_DIRECTORY => expr, + // LinuxKeyboardKeyCodes.KEY_LIST => expr, + // LinuxKeyboardKeyCodes.KEY_MEMO => expr, + // LinuxKeyboardKeyCodes.KEY_CALENDAR => expr, + // LinuxKeyboardKeyCodes.KEY_RED => expr, + // LinuxKeyboardKeyCodes.KEY_GREEN => expr, + // LinuxKeyboardKeyCodes.KEY_YELLOW => expr, + // LinuxKeyboardKeyCodes.KEY_BLUE => expr, + // LinuxKeyboardKeyCodes.KEY_CHANNELUP => expr, + // LinuxKeyboardKeyCodes.KEY_CHANNELDOWN => expr, + // LinuxKeyboardKeyCodes.KEY_FIRST => expr, + // LinuxKeyboardKeyCodes.KEY_LAST => expr, + // LinuxKeyboardKeyCodes.KEY_AB => expr, + // LinuxKeyboardKeyCodes.KEY_NEXT => expr, + // LinuxKeyboardKeyCodes.KEY_RESTART => expr, + // LinuxKeyboardKeyCodes.KEY_SLOW => expr, + // LinuxKeyboardKeyCodes.KEY_SHUFFLE => expr, + // LinuxKeyboardKeyCodes.KEY_BREAK => expr, + // LinuxKeyboardKeyCodes.KEY_PREVIOUS => expr, + // LinuxKeyboardKeyCodes.KEY_DIGITS => expr, + // LinuxKeyboardKeyCodes.KEY_TEEN => expr, + // LinuxKeyboardKeyCodes.KEY_TWEN => expr, + // LinuxKeyboardKeyCodes.KEY_VIDEOPHONE => expr, + // LinuxKeyboardKeyCodes.KEY_GAMES => expr, + // LinuxKeyboardKeyCodes.KEY_ZOOMIN => expr, + // LinuxKeyboardKeyCodes.KEY_ZOOMOUT => expr, + // LinuxKeyboardKeyCodes.KEY_ZOOMRESET => expr, + // LinuxKeyboardKeyCodes.KEY_WORDPROCESSOR => expr, + // LinuxKeyboardKeyCodes.KEY_EDITOR => expr, + // LinuxKeyboardKeyCodes.KEY_SPREADSHEET => expr, + // LinuxKeyboardKeyCodes.KEY_GRAPHICSEDITOR => expr, + // LinuxKeyboardKeyCodes.KEY_PRESENTATION => expr, + // LinuxKeyboardKeyCodes.KEY_DATABASE => expr, + // LinuxKeyboardKeyCodes.KEY_NEWS => expr, + // LinuxKeyboardKeyCodes.KEY_VOICEMAIL => expr, + // LinuxKeyboardKeyCodes.KEY_ADDRESSBOOK => expr, + // LinuxKeyboardKeyCodes.KEY_MESSENGER => expr, + // LinuxKeyboardKeyCodes.KEY_DISPLAYTOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_SPELLCHECK => expr, + // LinuxKeyboardKeyCodes.KEY_LOGOFF => expr, + // LinuxKeyboardKeyCodes.KEY_DOLLAR => expr, + // LinuxKeyboardKeyCodes.KEY_EURO => expr, + // LinuxKeyboardKeyCodes.KEY_FRAMEBACK => expr, + // LinuxKeyboardKeyCodes.KEY_FRAMEFORWARD => expr, + // LinuxKeyboardKeyCodes.KEY_CONTEXT_MENU => expr, + // LinuxKeyboardKeyCodes.KEY_MEDIA_REPEAT => expr, + // LinuxKeyboardKeyCodes.KEY_10CHANNELSUP => expr, + // LinuxKeyboardKeyCodes.KEY_10CHANNELSDOWN => expr, + // LinuxKeyboardKeyCodes.KEY_IMAGES => expr, + // LinuxKeyboardKeyCodes.KEY_NOTIFICATION_CENTER => expr, + // LinuxKeyboardKeyCodes.KEY_PICKUP_PHONE => expr, + // LinuxKeyboardKeyCodes.KEY_HANGUP_PHONE => expr, + // LinuxKeyboardKeyCodes.KEY_DEL_EOL => expr, + // LinuxKeyboardKeyCodes.KEY_DEL_EOS => expr, + // LinuxKeyboardKeyCodes.KEY_INS_LINE => expr, + // LinuxKeyboardKeyCodes.KEY_DEL_LINE => expr, + // LinuxKeyboardKeyCodes.KEY_FN => expr, + // LinuxKeyboardKeyCodes.KEY_FN_ESC => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F1 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F2 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F3 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F4 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F5 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F6 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F7 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F8 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F9 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F10 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F11 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F12 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_1 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_2 => expr, + // LinuxKeyboardKeyCodes.KEY_FN_D => expr, + // LinuxKeyboardKeyCodes.KEY_FN_E => expr, + // LinuxKeyboardKeyCodes.KEY_FN_F => expr, + // LinuxKeyboardKeyCodes.KEY_FN_S => expr, + // LinuxKeyboardKeyCodes.KEY_FN_B => expr, + // LinuxKeyboardKeyCodes.KEY_FN_RIGHT_SHIFT => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT1 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT2 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT3 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT4 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT5 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT6 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT7 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT8 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT9 => expr, + // LinuxKeyboardKeyCodes.KEY_BRL_DOT10 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_0 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_1 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_2 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_3 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_4 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_5 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_6 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_7 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_8 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_9 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_STAR => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_POUND => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_A => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_B => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_C => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_D => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_FOCUS => expr, + // LinuxKeyboardKeyCodes.KEY_WPS_BUTTON => expr, + // LinuxKeyboardKeyCodes.KEY_TOUCHPAD_TOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_TOUCHPAD_ON => expr, + // LinuxKeyboardKeyCodes.KEY_TOUCHPAD_OFF => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_ZOOMIN => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_ZOOMOUT => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_UP => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_DOWN => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_LEFT => expr, + // LinuxKeyboardKeyCodes.KEY_CAMERA_RIGHT => expr, + // LinuxKeyboardKeyCodes.KEY_ATTENDANT_ON => expr, + // LinuxKeyboardKeyCodes.KEY_ATTENDANT_OFF => expr, + // LinuxKeyboardKeyCodes.KEY_ATTENDANT_TOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_LIGHTS_TOGGLE => expr, + // LinuxKeyboardKeyCodes.BTN_DPAD_UP => expr, + // LinuxKeyboardKeyCodes.BTN_DPAD_DOWN => expr, + // LinuxKeyboardKeyCodes.BTN_DPAD_LEFT => expr, + // LinuxKeyboardKeyCodes.BTN_DPAD_RIGHT => expr, + // LinuxKeyboardKeyCodes.KEY_ALS_TOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_ROTATE_LOCK_TOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_BUTTONCONFIG => expr, + // LinuxKeyboardKeyCodes.KEY_TASKMANAGER => expr, + // LinuxKeyboardKeyCodes.KEY_JOURNAL => expr, + // LinuxKeyboardKeyCodes.KEY_CONTROLPANEL => expr, + // LinuxKeyboardKeyCodes.KEY_APPSELECT => expr, + // LinuxKeyboardKeyCodes.KEY_SCREENSAVER => expr, + // LinuxKeyboardKeyCodes.KEY_VOICECOMMAND => expr, + // LinuxKeyboardKeyCodes.KEY_ASSISTANT => expr, + // LinuxKeyboardKeyCodes.KEY_KBD_LAYOUT_NEXT => expr, + // LinuxKeyboardKeyCodes.KEY_EMOJI_PICKER => expr, + // LinuxKeyboardKeyCodes.KEY_BRIGHTNESS_MIN => expr, + // LinuxKeyboardKeyCodes.KEY_BRIGHTNESS_MAX => expr, + // LinuxKeyboardKeyCodes.KEY_KBDINPUTASSIST_PREV => expr, + // LinuxKeyboardKeyCodes.KEY_KBDINPUTASSIST_NEXT => expr, + // LinuxKeyboardKeyCodes.KEY_KBDINPUTASSIST_PREVGROUP => expr, + // LinuxKeyboardKeyCodes.KEY_KBDINPUTASSIST_NEXTGROUP => expr, + // LinuxKeyboardKeyCodes.KEY_KBDINPUTASSIST_ACCEPT => expr, + // LinuxKeyboardKeyCodes.KEY_KBDINPUTASSIST_CANCEL => expr, + // LinuxKeyboardKeyCodes.KEY_RIGHT_UP => expr, + // LinuxKeyboardKeyCodes.KEY_RIGHT_DOWN => expr, + // LinuxKeyboardKeyCodes.KEY_LEFT_UP => expr, + // LinuxKeyboardKeyCodes.KEY_LEFT_DOWN => expr, + // LinuxKeyboardKeyCodes.KEY_ROOT_MENU => expr, + // LinuxKeyboardKeyCodes.KEY_MEDIA_TOP_MENU => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_11 => expr, + // LinuxKeyboardKeyCodes.KEY_NUMERIC_12 => expr, + // LinuxKeyboardKeyCodes.KEY_AUDIO_DESC => expr, + // LinuxKeyboardKeyCodes.KEY_3D_MODE => expr, + // LinuxKeyboardKeyCodes.KEY_NEXT_FAVORITE => expr, + // LinuxKeyboardKeyCodes.KEY_STOP_RECORD => expr, + // LinuxKeyboardKeyCodes.KEY_PAUSE_RECORD => expr, + // LinuxKeyboardKeyCodes.KEY_VOD => expr, + // LinuxKeyboardKeyCodes.KEY_UNMUTE => expr, + // LinuxKeyboardKeyCodes.KEY_FASTREVERSE => expr, + // LinuxKeyboardKeyCodes.KEY_SLOWREVERSE => expr, + // LinuxKeyboardKeyCodes.KEY_DATA => expr, + // LinuxKeyboardKeyCodes.KEY_ONSCREEN_KEYBOARD => expr, + // LinuxKeyboardKeyCodes.KEY_PRIVACY_SCREEN_TOGGLE => expr, + // LinuxKeyboardKeyCodes.KEY_SELECTIVE_SCREENSHOT => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO1 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO2 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO3 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO4 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO5 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO6 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO7 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO8 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO9 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO10 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO11 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO12 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO13 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO14 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO15 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO16 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO17 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO18 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO19 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO20 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO21 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO22 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO23 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO24 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO25 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO26 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO27 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO28 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO29 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO30 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO_RECORD_START => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO_RECORD_STOP => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO_PRESET_CYCLE => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO_PRESET1 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO_PRESET2 => expr, + // LinuxKeyboardKeyCodes.KEY_MACRO_PRESET3 => expr, + // LinuxKeyboardKeyCodes.KEY_KBD_LCD_MENU1 => expr, + // LinuxKeyboardKeyCodes.KEY_KBD_LCD_MENU2 => expr, + // LinuxKeyboardKeyCodes.KEY_KBD_LCD_MENU3 => expr, + // LinuxKeyboardKeyCodes.KEY_KBD_LCD_MENU4 => expr, + // LinuxKeyboardKeyCodes.KEY_KBD_LCD_MENU5 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY2 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY3 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY4 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY5 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY6 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY7 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY8 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY9 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY10 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY11 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY12 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY13 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY14 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY15 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY16 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY17 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY18 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY19 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY20 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY21 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY22 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY23 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY24 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY25 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY26 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY27 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY28 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY29 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY30 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY31 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY32 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY33 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY34 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY35 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY36 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY37 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY38 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY39 => expr, + // LinuxKeyboardKeyCodes.BTN_TRIGGER_HAPPY40 => expr, + _ => KeyboardKey.None + }; + + + public static MouseButton MouseButtonFromButtonCode(LinuxKeyboardKeyCodes eCode) => eCode switch + { + LinuxKeyboardKeyCodes.BTN_LEFT => MouseButton.Left, + LinuxKeyboardKeyCodes.BTN_RIGHT => MouseButton.Right, + LinuxKeyboardKeyCodes.BTN_MIDDLE => MouseButton.Middle, + //todo: figure these out + // LinuxKeyboardKeyCodes.BTN_SIDE => expr, + // LinuxKeyboardKeyCodes.BTN_EXTRA => expr, + // LinuxKeyboardKeyCodes.BTN_FORWARD => expr, + // LinuxKeyboardKeyCodes.BTN_BACK => expr, + // LinuxKeyboardKeyCodes.BTN_TASK => expr, + _ => throw new ArgumentOutOfRangeException(nameof(eCode), eCode, null) + }; + } +} \ No newline at end of file From 0c3f84cbeb522cabe96c83a41c2bb16e2b8eac77 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 9 Dec 2021 23:32:59 +0100 Subject: [PATCH 095/270] Sidebar - Moved VMs and views Sidebar - Implemented profile selection Windows - Fix designer breaking due to the input provider --- .../Services/Input/InputProvider.cs | 1 + .../Services/Input/InputService.cs | 1 + .../Controls/HotkeyBox.axaml.cs | 3 +- src/Avalonia/Artemis.UI.Windows/App.axaml.cs | 7 +- .../Providers/Input/WindowsInputProvider.cs | 2 +- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 4 +- .../Ninject/Factories/IVMFactory.cs | 9 ++- .../ProfileEditor/ProfileEditorView.axaml | 7 +- .../ProfileEditor/ProfileEditorViewModel.cs | 24 ++++++- .../Artemis.UI/Screens/Root/RootViewModel.cs | 2 +- .../SidebarCategoryEditView.axaml | 2 +- .../SidebarCategoryEditView.axaml.cs | 2 +- .../SidebarCategoryEditViewModel.cs | 2 +- .../ProfileConfigurationEditView.axaml | 65 ++++++++++++++----- .../ProfileConfigurationEditView.axaml.cs | 3 +- .../ProfileConfigurationEditViewModel.cs | 33 +++++++++- .../Sidebar/Dialogs/ProfileIconViewModel.cs | 2 +- .../Sidebar/Dialogs/ProfileModuleViewModel.cs | 2 +- .../Sidebar/SidebarCategoryView.axaml | 4 +- .../Sidebar/SidebarCategoryView.axaml.cs | 2 +- .../Sidebar/SidebarCategoryViewModel.cs | 25 +++++-- .../SidebarProfileConfigurationView.axaml | 2 +- .../SidebarProfileConfigurationView.axaml.cs | 2 +- .../SidebarProfileConfigurationViewModel.cs | 4 +- .../Sidebar/SidebarScreenView.axaml | 2 +- .../Sidebar/SidebarScreenView.axaml.cs | 2 +- .../Sidebar/SidebarScreenViewModel.cs | 2 +- .../{Root => }/Sidebar/SidebarView.axaml | 2 +- .../{Root => }/Sidebar/SidebarView.axaml.cs | 2 +- .../{Root => }/Sidebar/SidebarViewModel.cs | 33 ++++++++-- .../Services/ProfileEditorService.cs | 51 +++++++++++++++ 31 files changed, 244 insertions(+), 60 deletions(-) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml (87%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs (90%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs (97%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/Dialogs/ProfileConfigurationEditView.axaml (71%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs (85%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs (87%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/Dialogs/ProfileIconViewModel.cs (86%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/Dialogs/ProfileModuleViewModel.cs (92%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarCategoryView.axaml (97%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarCategoryView.axaml.cs (93%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarCategoryViewModel.cs (78%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarProfileConfigurationView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarProfileConfigurationView.axaml.cs (89%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarProfileConfigurationViewModel.cs (94%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarScreenView.axaml (90%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarScreenView.axaml.cs (88%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarScreenViewModel.cs (96%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarView.axaml (98%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarView.axaml.cs (88%) rename src/Avalonia/Artemis.UI/Screens/{Root => }/Sidebar/SidebarViewModel.cs (75%) create mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditorService.cs diff --git a/src/Artemis.Core/Services/Input/InputProvider.cs b/src/Artemis.Core/Services/Input/InputProvider.cs index b10db3d6f..5565fc0ec 100644 --- a/src/Artemis.Core/Services/Input/InputProvider.cs +++ b/src/Artemis.Core/Services/Input/InputProvider.cs @@ -95,6 +95,7 @@ namespace Artemis.Core.Services MouseMoveDataReceived?.Invoke(this, new InputProviderMouseMoveEventArgs(device, cursorX, cursorY, deltaX, deltaY)); } + // TODO: Remove this as the core should handle this in GetDeviceByIdentifier /// /// Invokes the event which the listens to as long as /// this provider is registered diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index 510b095f7..f47ac5d78 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -98,6 +98,7 @@ namespace Artemis.Core.Services BustIdentifierCache(); } + // TODO: Move the OnIdentifierReceived logic into here and get rid of OnIdentifierReceived, this and OnIdentifierReceived are always called in combination with each other public ArtemisDevice? GetDeviceByIdentifier(InputProvider provider, object identifier, InputDeviceType type) { if (provider == null) throw new ArgumentNullException(nameof(provider)); diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs index a78ff19d5..1607738f0 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs @@ -4,6 +4,7 @@ using Artemis.Core; using Artemis.Core.Services; using Avalonia; using Avalonia.Controls; +using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; @@ -91,7 +92,7 @@ namespace Artemis.UI.Shared.Controls /// to an SVG /// public static readonly StyledProperty HotkeyProperty = - AvaloniaProperty.Register(nameof(Hotkey), notifying: HotkeyChanging); + AvaloniaProperty.Register(nameof(Hotkey), defaultBindingMode: BindingMode.TwoWay, notifying: HotkeyChanging); public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register(nameof(Watermark)); diff --git a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs index e37a0763f..6d5b42adc 100644 --- a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs @@ -1,5 +1,4 @@ using Artemis.Core.Services; -using Artemis.UI.Windows.Providers; using Artemis.UI.Windows.Providers.Input; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; @@ -17,15 +16,17 @@ namespace Artemis.UI.Windows _kernel = ArtemisBootstrapper.Bootstrap(this); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); - - RegisterProviders(_kernel); } public override void OnFrameworkInitializationCompleted() { ArtemisBootstrapper.Initialize(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { _applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args); + RegisterProviders(_kernel!); + } } private void RegisterProviders(StandardKernel standardKernel) diff --git a/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs b/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs index 4aca8e831..ef4f81bd9 100644 --- a/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs +++ b/src/Avalonia/Artemis.UI.Windows/Providers/Input/WindowsInputProvider.cs @@ -185,7 +185,7 @@ namespace Artemis.UI.Windows.Providers.Input if (identifier != null) try { - device = _inputService.GetDeviceByIdentifier(this, identifier, InputDeviceType.Keyboard); + device = _inputService.GetDeviceByIdentifier(this, identifier, InputDeviceType.Mouse); } catch (Exception e) { diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 39ccd647e..c8d210bf0 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -51,10 +51,10 @@ DebugSettingsView.axaml - + SidebarCategoryEditView.axaml - + ProfileConfigurationEditView.axaml diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 2248b2bdc..64b6a1704 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -3,9 +3,11 @@ using Artemis.Core; using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device.Tabs; using Artemis.UI.Screens.Plugins; -using Artemis.UI.Screens.Root.Sidebar; +using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.Settings.Tabs; +using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.SurfaceEditor; +using Artemis.UI.Services; using ReactiveUI; namespace Artemis.UI.Ninject.Factories @@ -49,4 +51,9 @@ namespace Artemis.UI.Ninject.Factories { PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); } + + public interface IProfileEditorVmFactory : IVmFactory + { + ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 5770a7a23..f29ad78f3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -4,5 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileEditorView"> - Welcome to Avalonia! + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 146bd4ed0..02c8b6734 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -1,8 +1,28 @@ -using Artemis.UI.Shared; +using System; +using System.Reactive.Disposables; +using Artemis.Core; +using Artemis.UI.Services; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor { - public class ProfileEditorViewModel : ActivatableViewModelBase + public class ProfileEditorViewModel : MainScreenViewModel { + private ProfileConfiguration? _profile; + + /// + public ProfileEditorViewModel(IScreen hostScreen, IProfileEditorService profileEditorService) : base(hostScreen, "profile-editor") + { + this.WhenActivated(disposables => + { + profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => Profile = p).DisposeWith(disposables); + }); + } + + public ProfileConfiguration? Profile + { + get => _profile; + set => this.RaiseAndSetIfChanged(ref _profile, value); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 68c7cd9be..1301bb454 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.Root.Sidebar; +using Artemis.UI.Screens.Sidebar; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml similarity index 87% rename from src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml rename to src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml index 2553bf0cf..e75f36ba3 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml @@ -3,7 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Root.Sidebar.Dialogs.SidebarCategoryEditView"> + x:Class="Artemis.UI.Screens.Sidebar.ContentDialogs.SidebarCategoryEditView"> diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs similarity index 90% rename from src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs index 5ebe8080d..4b5d1814e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml.cs @@ -3,7 +3,7 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using ReactiveUI; -namespace Artemis.UI.Screens.Root.Sidebar.Dialogs +namespace Artemis.UI.Screens.Sidebar.ContentDialogs { public class SidebarCategoryEditView : ReactiveUserControl { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs similarity index 97% rename from src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs index 428fdc46f..009eb2c34 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs @@ -6,7 +6,7 @@ using FluentAvalonia.UI.Controls; using ReactiveUI; using ReactiveUI.Validation.Extensions; -namespace Artemis.UI.Screens.Root.Sidebar.Dialogs +namespace Artemis.UI.Screens.Sidebar.ContentDialogs { public class SidebarCategoryEditViewModel : ContentDialogViewModelBase { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml similarity index 71% rename from src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml rename to src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml index ce58d9c59..b4733a770 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml @@ -4,15 +4,16 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" - xmlns:local="clr-namespace:Artemis.UI.Screens.Root.Sidebar.Dialogs" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" + xmlns:local="clr-namespace:Artemis.UI.Screens.Sidebar.Dialogs" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="850" - x:Class="Artemis.UI.Screens.Root.Sidebar.Dialogs.ProfileConfigurationEditView" + x:Class="Artemis.UI.Screens.Sidebar.Dialogs.ProfileConfigurationEditView" Title="{Binding DisplayName}" Icon="/Assets/Images/Logo/bow.ico" Width="800" + MinWidth="420" Height="850"> @@ -24,6 +25,14 @@ + + @@ -31,12 +40,10 @@ - General - Profile name - + Module @@ -71,7 +78,7 @@ Icon - + - - - + + + Workshop!! :3 + + + Notification tests + + + + - - - - - + + + + + \ No newline at end of file From bfbe29be638456248d15fd9c9ce2a6acef7180fd Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Dec 2021 19:42:37 +0100 Subject: [PATCH 097/270] Profile editor - WIPeroony --- .../Artemis.UI.Avalonia.Shared.xml | 5 + .../Artemis.UI.Linux/packages.lock.json | 20 ++-- .../Artemis.UI.MacOS/packages.lock.json | 20 ++-- .../Artemis.UI.Shared.csproj | 2 +- .../Artemis.UI.Shared/Styles/Artemis.axaml | 12 +++ .../Artemis.UI.Shared/Styles/TreeView.axaml | 99 +++++++++++++++++++ .../Artemis.UI.Shared/packages.lock.json | 6 +- .../Artemis.UI.Windows/packages.lock.json | 20 ++-- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 7 +- .../Artemis.UI/Artemis.UI.csproj.DotSettings | 1 + src/Avalonia/Artemis.UI/MainWindow.axaml | 15 ++- src/Avalonia/Artemis.UI/MainWindow.axaml.cs | 12 ++- .../Ninject/Factories/IVMFactory.cs | 3 + src/Avalonia/Artemis.UI/ReactiveCoreWindow.cs | 80 +++++++++++++++ .../Screens/Debugger/DebugView.axaml | 4 +- .../Screens/Debugger/DebugView.axaml.cs | 2 +- .../Screens/Device/DevicePropertiesView.axaml | 5 +- .../Device/DevicePropertiesView.axaml.cs | 2 +- .../Plugins/PluginSettingsWindowView.axaml | 5 +- .../Plugins/PluginSettingsWindowView.axaml.cs | 2 +- .../ProfileTree/FolderTreeItemView.axaml | 36 +++++++ .../ProfileTree/FolderTreeItemView.axaml.cs | 18 ++++ .../ProfileTree/FolderTreeItemViewModel.cs | 27 +++++ .../ProfileTree/LayerTreeItemView.axaml | 29 ++++++ .../ProfileTree/LayerTreeItemView.axaml.cs | 18 ++++ .../ProfileTree/LayerTreeItemViewModel.cs | 14 +++ .../Panels/ProfileTree/ProfileTreeView.axaml | 15 +++ .../ProfileTree/ProfileTreeView.axaml.cs | 18 ++++ .../ProfileTree/ProfileTreeViewModel.cs | 43 ++++++++ .../Panels/ProfileTree/TreeItemViewModel.cs | 8 ++ .../ProfileEditor/ProfileEditorView.axaml | 2 +- .../ProfileEditor/ProfileEditorViewModel.cs | 6 +- .../ProfileConfigurationEditView.axaml | 5 +- .../ProfileConfigurationEditView.axaml.cs | 2 +- src/Avalonia/Artemis.UI/Styles/Artemis.axaml | 1 + src/Avalonia/Artemis.UI/packages.lock.json | 20 ++-- 36 files changed, 520 insertions(+), 64 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml create mode 100644 src/Avalonia/Artemis.UI/ReactiveCoreWindow.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 3ee84f85d..02a5517ec 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -756,6 +756,11 @@ Closes the window hosting the view model + + + Called when the the window hosting the view model should close + + Occurs when the the window hosting the view model should close diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 0fc43dcba..666de545f 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -219,8 +219,8 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.1.6", - "contentHash": "EJukyiTmEVhaYlHdntFMyQKI4+u772rSClKYQqJRfkTb1NoJXLqiIVqMjx8ZQ0pxnfih+6CZ7+x82lfrGHIPUw==", + "resolved": "1.1.7", + "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", "dependencies": { "Avalonia": "0.10.10", "Avalonia.Desktop": "0.10.10", @@ -689,16 +689,16 @@ }, "Splat": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "7iW45RA7AbSlQPCgdokmysva5PGd6iBUhuNkC0XD73LF9dxfTkKeo3wZkohU7nvspDhJ7PJsYHvDtxIt5bMQ8Q==" + "resolved": "14.1.1", + "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "rTF0HSa6p8nxrXj2hwVgkutcTDJUXY34sY+zYK4ky65b7a0ROL8kdiYyxVVLE4Lq31N5Rcd4bBbqlPkgwZguww==", + "resolved": "14.1.1", + "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", "dependencies": { "Ninject": "3.3.4", - "Splat": "13.1.63" + "Splat": "14.1.1" } }, "Svg.Custom": { @@ -1692,12 +1692,12 @@ "Avalonia.Diagnostics": "0.10.10", "Avalonia.ReactiveUI": "0.10.10", "Avalonia.Svg.Skia": "0.10.10", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1", - "Splat.Ninject": "13.1.63" + "Splat.Ninject": "14.1.1" } }, "artemis.ui.shared": { @@ -1710,7 +1710,7 @@ "Avalonia.Xaml.Behaviors": "0.10.10.4", "Avalonia.Xaml.Interactions": "0.10.10.4", "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1" } diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 4571535af..e9d390ff7 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -210,8 +210,8 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.1.6", - "contentHash": "EJukyiTmEVhaYlHdntFMyQKI4+u772rSClKYQqJRfkTb1NoJXLqiIVqMjx8ZQ0pxnfih+6CZ7+x82lfrGHIPUw==", + "resolved": "1.1.7", + "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", "dependencies": { "Avalonia": "0.10.10", "Avalonia.Desktop": "0.10.10", @@ -688,16 +688,16 @@ }, "Splat": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "7iW45RA7AbSlQPCgdokmysva5PGd6iBUhuNkC0XD73LF9dxfTkKeo3wZkohU7nvspDhJ7PJsYHvDtxIt5bMQ8Q==" + "resolved": "14.1.1", + "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "rTF0HSa6p8nxrXj2hwVgkutcTDJUXY34sY+zYK4ky65b7a0ROL8kdiYyxVVLE4Lq31N5Rcd4bBbqlPkgwZguww==", + "resolved": "14.1.1", + "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", "dependencies": { "Ninject": "3.3.4", - "Splat": "13.1.63" + "Splat": "14.1.1" } }, "Svg.Custom": { @@ -1691,12 +1691,12 @@ "Avalonia.Diagnostics": "0.10.10", "Avalonia.ReactiveUI": "0.10.10", "Avalonia.Svg.Skia": "0.10.10", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1", - "Splat.Ninject": "13.1.63" + "Splat.Ninject": "14.1.1" } }, "artemis.ui.shared": { @@ -1709,7 +1709,7 @@ "Avalonia.Xaml.Behaviors": "0.10.10.4", "Avalonia.Xaml.Interactions": "0.10.10.4", "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1" } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 180b48704..63cb328e3 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index f9f0aaa72..5bcd365a3 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -27,4 +27,16 @@ + + + + + diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml new file mode 100644 index 000000000..df6fa92d6 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml @@ -0,0 +1,99 @@ + + + + + + + + Test + + + + + + + + + Test + + + + + + + + + Test + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index df39f637e..702356de1 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -72,9 +72,9 @@ }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.1.6, )", - "resolved": "1.1.6", - "contentHash": "EJukyiTmEVhaYlHdntFMyQKI4+u772rSClKYQqJRfkTb1NoJXLqiIVqMjx8ZQ0pxnfih+6CZ7+x82lfrGHIPUw==", + "requested": "[1.1.7, )", + "resolved": "1.1.7", + "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", "dependencies": { "Avalonia": "0.10.10", "Avalonia.Desktop": "0.10.10", diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index c2d37b9eb..8ac82b3ba 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -226,8 +226,8 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.1.6", - "contentHash": "EJukyiTmEVhaYlHdntFMyQKI4+u772rSClKYQqJRfkTb1NoJXLqiIVqMjx8ZQ0pxnfih+6CZ7+x82lfrGHIPUw==", + "resolved": "1.1.7", + "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", "dependencies": { "Avalonia": "0.10.10", "Avalonia.Desktop": "0.10.10", @@ -704,16 +704,16 @@ }, "Splat": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "7iW45RA7AbSlQPCgdokmysva5PGd6iBUhuNkC0XD73LF9dxfTkKeo3wZkohU7nvspDhJ7PJsYHvDtxIt5bMQ8Q==" + "resolved": "14.1.1", + "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "rTF0HSa6p8nxrXj2hwVgkutcTDJUXY34sY+zYK4ky65b7a0ROL8kdiYyxVVLE4Lq31N5Rcd4bBbqlPkgwZguww==", + "resolved": "14.1.1", + "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", "dependencies": { "Ninject": "3.3.4", - "Splat": "13.1.63" + "Splat": "14.1.1" } }, "Svg.Custom": { @@ -1707,12 +1707,12 @@ "Avalonia.Diagnostics": "0.10.10", "Avalonia.ReactiveUI": "0.10.10", "Avalonia.Svg.Skia": "0.10.10", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1", - "Splat.Ninject": "13.1.63" + "Splat.Ninject": "14.1.1" } }, "artemis.ui.shared": { @@ -1725,7 +1725,7 @@ "Avalonia.Xaml.Behaviors": "0.10.10.4", "Avalonia.Xaml.Interactions": "0.10.10.4", "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1" } diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index c8d210bf0..8f59f87df 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -19,12 +19,12 @@ - + - + @@ -58,4 +58,7 @@ ProfileConfigurationEditView.axaml + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings b/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings index 9bdec1f7b..f3b6b6826 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj.DotSettings @@ -4,6 +4,7 @@ True True True + True True True True \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml b/src/Avalonia/Artemis.UI/MainWindow.axaml index 2a507bddd..a3470a84d 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml @@ -1,13 +1,22 @@ - - + + + + Test + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml.cs b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs index ad4cf0110..f1e173403 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml.cs +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs @@ -1,20 +1,30 @@ using Artemis.UI.Screens.Root; using Avalonia; +using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using FluentAvalonia.Core.ApplicationModel; namespace Artemis.UI { - public class MainWindow : ReactiveWindow + public class MainWindow : ReactiveCoreWindow { public MainWindow() { InitializeComponent(); + SetupTitlebar(); #if DEBUG this.AttachDevTools(); #endif } + private void SetupTitlebar() + { + object? titleBar = this.FindResource("RootWindowTitlebar"); + if (titleBar != null) + SetTitleBar((IControl) titleBar); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index bd173bd57..7285e9802 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -3,6 +3,7 @@ using Artemis.Core; using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor; +using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.SurfaceEditor; @@ -54,5 +55,7 @@ namespace Artemis.UI.Ninject.Factories public interface IProfileEditorVmFactory : IVmFactory { ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); + FolderTreeItemViewModel FolderTreeItemViewModel(Folder folder); + LayerTreeItemViewModel LayerTreeItemViewModel(Layer layer); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/ReactiveCoreWindow.cs b/src/Avalonia/Artemis.UI/ReactiveCoreWindow.cs new file mode 100644 index 000000000..5e5a8eb67 --- /dev/null +++ b/src/Avalonia/Artemis.UI/ReactiveCoreWindow.cs @@ -0,0 +1,80 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using FluentAvalonia.Interop; +using FluentAvalonia.UI.Controls; +using ReactiveUI; + +namespace Artemis.UI +{ + /// + /// A ReactiveUI that implements the interface and will + /// activate your ViewModel automatically if the view model implements . When + /// the DataContext property changes, this class will update the ViewModel property with the new DataContext value, + /// and vice versa. + /// + /// ViewModel type. + public class ReactiveCoreWindow : CoreWindow, IViewFor where TViewModel : class + { + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty + .Register, TViewModel?>(nameof(ViewModel)); + + /// + /// Initializes a new instance of the class. + /// + public ReactiveCoreWindow() + { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + this.GetObservable(DataContextProperty).Subscribe(OnDataContextChanged); + this.GetObservable(ViewModelProperty).Subscribe(OnViewModelChanged); + + // TODO Remove + Win32Interop.OSVERSIONINFOEX version = default; + Win32Interop.RtlGetVersion(ref version); + if (version.MajorVersion == 10) + PseudoClasses.Add(":windows10"); + } + + /// + /// The ViewModel. + /// + public TViewModel? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + + private void OnDataContextChanged(object? value) + { + if (value is TViewModel viewModel) + { + ViewModel = viewModel; + } + else + { + ViewModel = null; + } + } + + private void OnViewModelChanged(object? value) + { + if (value == null) + { + ClearValue(DataContextProperty); + } + else if (DataContext != value) + { + DataContext = value; + } + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml index 4cb14d0c0..6c7eff869 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml @@ -1,4 +1,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs index 5d4428442..0709047e5 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml.cs @@ -12,7 +12,7 @@ using ReactiveUI; namespace Artemis.UI.Screens.Debugger { - public class DebugView : ReactiveWindow + public class DebugView : ReactiveCoreWindow { public DebugView() { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml index 861a997aa..96cdfc9b7 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml @@ -1,8 +1,9 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs index d525d7c7f..b5fd26391 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml.cs @@ -4,7 +4,7 @@ using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Device { - public partial class DevicePropertiesView : ReactiveWindow + public partial class DevicePropertiesView : ReactiveCoreWindow { public DevicePropertiesView() { diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 30caafb04..988cb4734 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -1,7 +1,8 @@ - - + diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs index 67a67d977..5828b0d0b 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs @@ -8,7 +8,7 @@ using ReactiveUI; namespace Artemis.UI.Screens.Plugins { - public class PluginSettingsWindowView : ReactiveWindow + public class PluginSettingsWindowView : ReactiveCoreWindow { public PluginSettingsWindowView() { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml new file mode 100644 index 000000000..bf7ae3c80 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml @@ -0,0 +1,36 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs new file mode 100644 index 000000000..cbd0ff111 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public partial class FolderTreeItemView : UserControl + { + public FolderTreeItemView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs new file mode 100644 index 000000000..ae0f11a59 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Artemis.Core; +using Artemis.UI.Ninject.Factories; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public class FolderTreeItemViewModel : TreeItemViewModel + { + public Folder Folder { get; } + + public FolderTreeItemViewModel(Folder folder, IProfileEditorVmFactory profileEditorVmFactory) + { + Folder = folder; + Children = new List(); + + foreach (ProfileElement profileElement in folder.Children) + { + if (profileElement is Folder childFolder) + Children.Add(profileEditorVmFactory.FolderTreeItemViewModel(childFolder)); + else if (profileElement is Layer layer) + Children.Add(profileEditorVmFactory.LayerTreeItemViewModel(layer)); + } + } + + public List Children { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml new file mode 100644 index 000000000..5b360cb54 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs new file mode 100644 index 000000000..362581616 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public partial class LayerTreeItemView : UserControl + { + public LayerTreeItemView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs new file mode 100644 index 000000000..e38f373d2 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -0,0 +1,14 @@ +using Artemis.Core; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public class LayerTreeItemViewModel : TreeItemViewModel + { + public Layer Layer { get; } + + public LayerTreeItemViewModel(Layer layer) + { + Layer = layer; + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml new file mode 100644 index 000000000..51a3df5ff --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml.cs new file mode 100644 index 000000000..8f74e4ba7 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public partial class ProfileTreeView : ReactiveUserControl + { + public ProfileTreeView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs new file mode 100644 index 000000000..1d1ddb99e --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using Artemis.Core; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Services; +using Artemis.UI.Shared; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public class ProfileTreeViewModel : ActivatableViewModelBase + { + private readonly IProfileEditorVmFactory _profileEditorVmFactory; + + public ProfileTreeViewModel(IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) + { + _profileEditorVmFactory = profileEditorVmFactory; + this.WhenActivated(d => profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(Repopulate).DisposeWith(d)); + } + + public ObservableCollection TreeItems { get; } = new(); + + private void Repopulate(ProfileConfiguration profileConfiguration) + { + if (TreeItems.Any()) + TreeItems.Clear(); + + if (profileConfiguration.Profile == null) + return; + + foreach (ProfileElement profileElement in profileConfiguration.Profile.GetRootFolder().Children) + { + if (profileElement is Folder folder) + TreeItems.Add(_profileEditorVmFactory.FolderTreeItemViewModel(folder)); + else if (profileElement is Layer layer) + TreeItems.Add(_profileEditorVmFactory.LayerTreeItemViewModel(layer)); + } + + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs new file mode 100644 index 000000000..28397234e --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -0,0 +1,8 @@ +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +{ + public abstract class TreeItemViewModel : ActivatableViewModelBase + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 5c38fd817..a64869c3d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -253,7 +253,7 @@ - Profile elements + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 73cd9f3f1..da3b0978b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Services; using ReactiveUI; @@ -12,13 +13,16 @@ namespace Artemis.UI.Screens.ProfileEditor private ProfileConfiguration? _profile; /// - public ProfileEditorViewModel(IScreen hostScreen, VisualEditorViewModel visualEditorViewModel, IProfileEditorService profileEditorService) : base(hostScreen, "profile-editor") + public ProfileEditorViewModel(IScreen hostScreen, IProfileEditorService profileEditorService, VisualEditorViewModel visualEditorViewModel, ProfileTreeViewModel profileTreeViewModel) + : base(hostScreen, "profile-editor") { VisualEditorViewModel = visualEditorViewModel; + ProfileTreeViewModel = profileTreeViewModel; this.WhenActivated(disposables => { profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => Profile = p).DisposeWith(disposables); }); } public VisualEditorViewModel VisualEditorViewModel { get; } + public ProfileTreeViewModel ProfileTreeViewModel { get; } public ProfileConfiguration? Profile { diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml index 6280b0825..eca9aa119 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml @@ -1,4 +1,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs index ce905a533..cb31ee232 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs @@ -4,7 +4,7 @@ using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Sidebar { - public partial class ProfileConfigurationEditView : ReactiveWindow + public partial class ProfileConfigurationEditView : ReactiveCoreWindow { public ProfileConfigurationEditView() { diff --git a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml index 7c51cbf0f..de19a9df5 100644 --- a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml @@ -10,6 +10,7 @@ + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index fed744e4f..627d2a480 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -76,9 +76,9 @@ }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.1.6, )", - "resolved": "1.1.6", - "contentHash": "EJukyiTmEVhaYlHdntFMyQKI4+u772rSClKYQqJRfkTb1NoJXLqiIVqMjx8ZQ0pxnfih+6CZ7+x82lfrGHIPUw==", + "requested": "[1.1.7, )", + "resolved": "1.1.7", + "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", "dependencies": { "Avalonia": "0.10.10", "Avalonia.Desktop": "0.10.10", @@ -126,12 +126,12 @@ }, "Splat.Ninject": { "type": "Direct", - "requested": "[13.1.63, )", - "resolved": "13.1.63", - "contentHash": "rTF0HSa6p8nxrXj2hwVgkutcTDJUXY34sY+zYK4ky65b7a0ROL8kdiYyxVVLE4Lq31N5Rcd4bBbqlPkgwZguww==", + "requested": "[14.1.1, )", + "resolved": "14.1.1", + "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", "dependencies": { "Ninject": "3.3.4", - "Splat": "13.1.63" + "Splat": "14.1.1" } }, "Avalonia.Angle.Windows.Natives": { @@ -705,8 +705,8 @@ }, "Splat": { "type": "Transitive", - "resolved": "13.1.63", - "contentHash": "7iW45RA7AbSlQPCgdokmysva5PGd6iBUhuNkC0XD73LF9dxfTkKeo3wZkohU7nvspDhJ7PJsYHvDtxIt5bMQ8Q==" + "resolved": "14.1.1", + "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" }, "Svg.Custom": { "type": "Transitive", @@ -1698,7 +1698,7 @@ "Avalonia.Xaml.Behaviors": "0.10.10.4", "Avalonia.Xaml.Interactions": "0.10.10.4", "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.6", + "FluentAvaloniaUI": "1.1.7", "Material.Icons.Avalonia": "1.0.2", "ReactiveUI.Validation": "2.2.1" } From 6569558df418876e497bd5f7abf4fa9f4a083790 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 27 Dec 2021 20:13:51 +0100 Subject: [PATCH 098/270] Meta - Updated Nuget packages Meta - Moved to RGB.NET pre-release Nuget packages --- .../Artemis.UI.Console.csproj | 26 - src/Artemis.ConsoleUI/Program.cs | 53 - .../Properties/PublishProfiles/Linux.pubxml | 16 - src/Artemis.ConsoleUI/packages.lock.json | 1247 ----------------- src/Artemis.Core/Artemis.Core.csproj | 14 +- src/Artemis.Core/packages.lock.json | 24 + .../Artemis.UI.Shared.csproj | 6 +- src/Artemis.UI.Shared/packages.lock.json | 25 + src/Artemis.UI/Artemis.UI.csproj | 7 +- src/Artemis.UI/packages.lock.json | 26 + .../packages.lock.json | 25 + src/Artemis.sln | 21 +- .../Artemis.UI.Linux/Artemis.UI.Linux.csproj | 9 +- .../Artemis.UI.Linux/packages.lock.json | 318 +++-- .../Artemis.UI.MacOS/Artemis.UI.MacOS.csproj | 8 +- .../Artemis.UI.MacOS/packages.lock.json | 307 ++-- .../Artemis.UI.Shared.csproj | 18 +- .../Artemis.UI.Shared/packages.lock.json | 265 ++-- .../Artemis.UI.Windows.csproj | 10 +- .../Artemis.UI.Windows/packages.lock.json | 309 ++-- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 23 +- src/Avalonia/Artemis.UI/MainWindow.axaml.cs | 6 +- src/Avalonia/Artemis.UI/packages.lock.json | 299 ++-- 23 files changed, 1049 insertions(+), 2013 deletions(-) delete mode 100644 src/Artemis.ConsoleUI/Artemis.UI.Console.csproj delete mode 100644 src/Artemis.ConsoleUI/Program.cs delete mode 100644 src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml delete mode 100644 src/Artemis.ConsoleUI/packages.lock.json diff --git a/src/Artemis.ConsoleUI/Artemis.UI.Console.csproj b/src/Artemis.ConsoleUI/Artemis.UI.Console.csproj deleted file mode 100644 index 145ff4a6a..000000000 --- a/src/Artemis.ConsoleUI/Artemis.UI.Console.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - exe - net5.0 - x64 - Artemis.UI.Console - Artemis.UI.Console - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.ConsoleUI/Program.cs b/src/Artemis.ConsoleUI/Program.cs deleted file mode 100644 index 6d694d8d6..000000000 --- a/src/Artemis.ConsoleUI/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using Artemis.Core; -using Artemis.Core.Ninject; -using Artemis.Core.Services; -using Artemis.Storage; -using Ninject; - -namespace Artemis.UI.Console -{ - /// - /// This is just a little experiment to show that Artemis can run without the UI and even on other OSes - /// Some notes - /// - Any plugin relying on WPF and/or Artemis.UI.Shared won't load - /// - There is no input provider so key-press events and brushes won't work - /// - Device providers using Windows SDKs won't work, OpenRGB will though! - /// - You may need to fiddle around to get SkiaSharp binaries going - /// - There is no UI obviously - /// - internal class Program - { - private static readonly AutoResetEvent Closing = new(false); - - protected static void OnExit(object sender, ConsoleCancelEventArgs args) - { - Closing.Set(); - } - - private static void Main(string[] args) - { - StorageManager.CreateBackup(Constants.DataFolder); - - Utilities.PrepareFirstLaunch(); - Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; - StandardKernel kernel = new() {Settings = {InjectNonPublic = true}}; - kernel.Load(); - - ICoreService core = kernel.Get(); - core.StartupArguments = args.ToList(); - core.IsElevated = false; - core.Initialize(); - - System.Console.CancelKeyPress += OnExit; - Closing.WaitOne(); - } - - private static void UtilitiesOnShutdownRequested(object sender, EventArgs e) - { - Closing.Set(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml b/src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml deleted file mode 100644 index 36fac7487..000000000 --- a/src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Release - x64 - bin\publish\linux - FileSystem - net5.0 - linux-x64 - false - False - - \ No newline at end of file diff --git a/src/Artemis.ConsoleUI/packages.lock.json b/src/Artemis.ConsoleUI/packages.lock.json deleted file mode 100644 index 2bb2a6632..000000000 --- a/src/Artemis.ConsoleUI/packages.lock.json +++ /dev/null @@ -1,1247 +0,0 @@ -{ - "version": 1, - "dependencies": { - ".NETCoreApp,Version=v5.0": { - "Humanizer.Core": { - "type": "Direct", - "requested": "[2.11.10, )", - "resolved": "2.11.10", - "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" - }, - "Ninject": { - "type": "Direct", - "requested": "[3.3.4, )", - "resolved": "3.3.4", - "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", - "dependencies": { - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0" - } - }, - "Ninject.Extensions.Conventions": { - "type": "Direct", - "requested": "[3.3.0, )", - "resolved": "3.3.0", - "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", - "dependencies": { - "Ninject.Extensions.Factory": "3.3.2" - } - }, - "Serilog": { - "type": "Direct", - "requested": "[2.10.0, )", - "resolved": "2.10.0", - "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" - }, - "System.Buffers": { - "type": "Direct", - "requested": "[4.5.1, )", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, - "System.ComponentModel.Annotations": { - "type": "Direct", - "requested": "[5.0.0, )", - "resolved": "5.0.0", - "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" - }, - "System.Numerics.Vectors": { - "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" - }, - "System.ValueTuple": { - "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "4.2.0", - "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", - "dependencies": { - "NETStandard.Library": "1.6.1", - "System.Collections.Specialized": "4.3.0", - "System.ComponentModel": "4.3.0", - "System.ComponentModel.TypeConverter": "4.3.0", - "System.Diagnostics.TraceSource": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Xml.XmlDocument": "4.3.0" - } - }, - "EmbedIO": { - "type": "Transitive", - "resolved": "3.4.3", - "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", - "dependencies": { - "Unosquare.Swan.Lite": "3.0.0" - } - }, - "HidSharp": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" - }, - "LiteDB": { - "type": "Transitive", - "resolved": "5.0.11", - "contentHash": "6cL4bOmVCUB0gIK+6qIr68HeqjjHZicPDGQjvJ87mIOvkFsEsJWkIps3yoKNeLpHhJQur++yoQ9Q8gxsdos0xQ==" - }, - "McMaster.NETCore.Plugins": { - "type": "Transitive", - "resolved": "1.4.0", - "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", - "dependencies": { - "Microsoft.DotNet.PlatformAbstractions": "3.1.6", - "Microsoft.Extensions.DependencyModel": "5.0.0" - } - }, - "Microsoft.DotNet.PlatformAbstractions": { - "type": "Transitive", - "resolved": "3.1.6", - "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "1.6.1", - "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.AppContext": "4.3.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Console": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.Compression.ZipFile": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.Net.Http": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Net.Sockets": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Timer": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0", - "System.Xml.XDocument": "4.3.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.1", - "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" - }, - "Ninject.Extensions.ChildKernel": { - "type": "Transitive", - "resolved": "3.3.0", - "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", - "dependencies": { - "Ninject": "3.3.4" - } - }, - "Ninject.Extensions.Factory": { - "type": "Transitive", - "resolved": "3.3.2", - "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", - "dependencies": { - "Castle.Core": "4.2.0", - "Ninject": "3.3.3" - } - }, - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" - }, - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" - }, - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", - "dependencies": { - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" - } - }, - "runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", - "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" - }, - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" - }, - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" - }, - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" - }, - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" - }, - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" - }, - "Serilog.Sinks.Console": { - "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", - "dependencies": { - "Serilog": "2.10.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", - "dependencies": { - "Serilog": "2.10.0" - } - }, - "Serilog.Sinks.File": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", - "dependencies": { - "Serilog": "2.10.0" - } - }, - "SkiaSharp": { - "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", - "dependencies": { - "System.Memory": "4.5.3" - } - }, - "System.AppContext": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Collections.NonGeneric": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Collections.Specialized": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", - "dependencies": { - "System.Collections.NonGeneric": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.ComponentModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.ComponentModel.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", - "dependencies": { - "System.ComponentModel": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.ComponentModel.TypeConverter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Collections.NonGeneric": "4.3.0", - "System.Collections.Specialized": "4.3.0", - "System.ComponentModel": "4.3.0", - "System.ComponentModel.Primitives": "4.3.0", - "System.Globalization": "4.3.0", - "System.Linq": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Console": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Diagnostics.Tools": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.TraceSource": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Dynamic.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Linq": "4.3.0", - "System.Linq.Expressions": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.Compression": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Buffers": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.IO.Compression": "4.3.0" - } - }, - "System.IO.Compression.ZipFile": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", - "dependencies": { - "System.Buffers": "4.3.0", - "System.IO": "4.3.0", - "System.IO.Compression": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", - "dependencies": { - "System.Security.AccessControl": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Linq.Expressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Linq": "4.3.0", - "System.ObjectModel": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Emit.Lightweight": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" - }, - "System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.DiagnosticSource": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Net.Sockets": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.ObjectModel": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.ILGeneration": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Emit.Lightweight": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "System.Runtime.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Runtime.InteropServices.RuntimeInformation": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Reflection.Extensions": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", - "dependencies": { - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Security.AccessControl": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "5.0.0", - "System.Security.Principal.Windows": "5.0.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.Apple": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Linq": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", - "dependencies": { - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Cng": "4.3.0", - "System.Security.Cryptography.Csp": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Text.Encoding.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Threading.Tasks.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Threading.Timer": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Xml.ReaderWriter": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Text.Encoding.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.Tasks.Extensions": "4.3.0" - } - }, - "System.Xml.XDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tools": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "System.Xml.XmlDocument": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Xml.ReaderWriter": "4.3.0" - } - }, - "Unosquare.Swan.Lite": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" - }, - "artemis.core": { - "type": "Project", - "dependencies": { - "Artemis.Storage": "1.0.0", - "EmbedIO": "3.4.3", - "HidSharp": "2.1.0", - "Humanizer.Core": "2.11.10", - "LiteDB": "5.0.11", - "McMaster.NETCore.Plugins": "1.4.0", - "Newtonsoft.Json": "13.0.1", - "Ninject": "3.3.4", - "Ninject.Extensions.ChildKernel": "3.3.0", - "Ninject.Extensions.Conventions": "3.3.0", - "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", - "Serilog.Sinks.Debug": "2.0.0", - "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", - "System.Buffers": "4.5.1", - "System.IO.FileSystem.AccessControl": "5.0.0", - "System.Numerics.Vectors": "4.5.0", - "System.Reflection.Metadata": "5.0.0", - "System.ValueTuple": "4.5.0" - } - }, - "artemis.storage": { - "type": "Project", - "dependencies": { - "LiteDB": "5.0.11", - "Serilog": "2.10.0" - } - } - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index b96547b2f..2ae3331e3 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -47,6 +47,9 @@ + + + @@ -62,17 +65,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Presets.dll - - PreserveNewest diff --git a/src/Artemis.Core/packages.lock.json b/src/Artemis.Core/packages.lock.json index 85eb5a7b3..faf4e21fb 100644 --- a/src/Artemis.Core/packages.lock.json +++ b/src/Artemis.Core/packages.lock.json @@ -73,6 +73,30 @@ "Ninject.Extensions.Factory": "3.3.2" } }, + "RGB.NET.Core": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "Serilog": { "type": "Direct", "requested": "[2.10.0, )", diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 6d9dc0fe0..ef0be9900 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -39,6 +39,7 @@ + @@ -69,11 +70,6 @@ false - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index fb3492af0..1fe62bb2f 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -52,6 +52,12 @@ "Ninject.Extensions.Factory": "3.3.2" } }, + "RGB.NET.Core": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, "SharpVectors.Reloaded": { "type": "Direct", "requested": "[1.7.5, )", @@ -264,6 +270,22 @@ "Ninject": "3.3.3" } }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -1303,6 +1325,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 4576072f0..abf861767 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -57,12 +57,6 @@ - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll - @@ -152,6 +146,7 @@ + diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 0c9eb0d1f..e5ce3ca56 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -122,6 +122,12 @@ "NETStandard.Library": "1.6.1" } }, + "RGB.NET.Core": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, "Serilog": { "type": "Direct", "requested": "[2.10.0, )", @@ -395,6 +401,22 @@ "Microsoft.Extensions.ObjectPool": "5.0.9" } }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -1449,6 +1471,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", @@ -1478,6 +1503,7 @@ "Microsoft.Xaml.Behaviors.Wpf": "1.1.31", "Ninject": "3.3.4", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", "SharpVectors.Reloaded": "1.7.5", "SkiaSharp": "2.80.3", "SkiaSharp.Views.WPF": "2.80.3", diff --git a/src/Artemis.VisualScripting/packages.lock.json b/src/Artemis.VisualScripting/packages.lock.json index 6c867521c..a0c2a9431 100644 --- a/src/Artemis.VisualScripting/packages.lock.json +++ b/src/Artemis.VisualScripting/packages.lock.json @@ -246,6 +246,27 @@ "Ninject": "3.3.3" } }, + "RGB.NET.Core": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -1309,6 +1330,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", @@ -1338,6 +1362,7 @@ "Microsoft.Xaml.Behaviors.Wpf": "1.1.31", "Ninject": "3.3.4", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", "SharpVectors.Reloaded": "1.7.5", "SkiaSharp": "2.80.3", "SkiaSharp.Views.WPF": "2.80.3", diff --git a/src/Artemis.sln b/src/Artemis.sln index 0f40efd73..46d3ed238 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28729.10 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI", "Artemis.UI\Artemis.UI.csproj", "{46B74153-77CF-4489-BDF9-D53FDB1F7ACB}" EndProject @@ -11,8 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.Core", "Artemis.Cor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Shared", "Artemis.UI.Shared\Artemis.UI.Shared.csproj", "{ADB357E6-151D-4D0D-87CB-68FD0BC29812}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Console", "Artemis.ConsoleUI\Artemis.UI.Console.csproj", "{ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI", "Avalonia\Artemis.UI\Artemis.UI.csproj", "{035CBB38-7B9E-4375-A39C-E9A5B01F23A5}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Shared", "Avalonia\Artemis.UI.Shared\Artemis.UI.Shared.csproj", "{05A5AB0F-A303-4404-9623-4DB1C9AA1DA0}" @@ -21,11 +19,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", " EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Avalonia", "Avalonia", "{960CAAC5-AA73-49F5-BF2F-DF2C789DF042}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.Windows", "Avalonia\Artemis.UI.Windows\Artemis.UI.Windows.csproj", "{DE45A288-9320-461F-BE2A-26DFE3817216}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Windows", "Avalonia\Artemis.UI.Windows\Artemis.UI.Windows.csproj", "{DE45A288-9320-461F-BE2A-26DFE3817216}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.Linux", "Avalonia\Artemis.UI.Linux\Artemis.UI.Linux.csproj", "{9012C8E2-3BEC-42F5-8270-7352A5922B04}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.Linux", "Avalonia\Artemis.UI.Linux\Artemis.UI.Linux.csproj", "{9012C8E2-3BEC-42F5-8270-7352A5922B04}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.UI.MacOS", "Avalonia\Artemis.UI.MacOS\Artemis.UI.MacOS.csproj", "{2F5F16DC-FACF-4559-9882-37C2949814C7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.MacOS", "Avalonia\Artemis.UI.MacOS\Artemis.UI.MacOS.csproj", "{2F5F16DC-FACF-4559-9882-37C2949814C7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -59,12 +57,6 @@ Global {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Release|Any CPU.ActiveCfg = Release|x64 {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Release|x64.ActiveCfg = Release|x64 {ADB357E6-151D-4D0D-87CB-68FD0BC29812}.Release|x64.Build.0 = Release|x64 - {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Debug|Any CPU.ActiveCfg = Debug|x64 - {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Debug|x64.ActiveCfg = Debug|x64 - {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Debug|x64.Build.0 = Debug|x64 - {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Release|Any CPU.ActiveCfg = Release|x64 - {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Release|x64.ActiveCfg = Release|x64 - {ACCC50FD-611A-41C4-B58F-DDC80B47D0CF}.Release|x64.Build.0 = Release|x64 {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {035CBB38-7B9E-4375-A39C-E9A5B01F23A5}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -116,11 +108,8 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {46B74153-77CF-4489-BDF9-D53FDB1F7ACB} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} - {ADB357E6-151D-4D0D-87CB-68FD0BC29812} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} {035CBB38-7B9E-4375-A39C-E9A5B01F23A5} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} {05A5AB0F-A303-4404-9623-4DB1C9AA1DA0} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} - {CF125C61-FD85-47EE-AF64-38B8F90DD50C} = {EE9A3079-7BFE-4E7B-A3D4-D3501C9A874A} {DE45A288-9320-461F-BE2A-26DFE3817216} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} {9012C8E2-3BEC-42F5-8270-7352A5922B04} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} {2F5F16DC-FACF-4559-9882-37C2949814C7} = {960CAAC5-AA73-49F5-BF2F-DF2C789DF042} diff --git a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 002253126..69e1b2744 100644 --- a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -9,12 +9,11 @@ - - + + - - - + + diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 666de545f..bb01b4430 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -4,11 +4,11 @@ ".NETCoreApp,Version=v5.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "2PSE+dB4vGJfG+1M+y+Hwaxiqze5mbBTTG9hjwc2Z3U/9yJE/GThBEst2WwI0yBt13hsfAfbABzt1PA3mtbFdw==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,49 +19,40 @@ }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "PQTl4lm7IZidzltMwC7RSNaoz7TYNznU8SKa/WaAI6ycMzC0On2DsqiL1dXr6WhYzMazyMJj6kBhiQzHIc1lIQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Native": "0.10.10", - "Avalonia.Skia": "0.10.10", - "Avalonia.Win32": "0.10.10", - "Avalonia.X11": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Native": "0.10.11", + "Avalonia.Skia": "0.10.11", + "Avalonia.Win32": "0.10.11", + "Avalonia.X11": "0.10.11" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "xBvBkF2DBKjddAfQbExd660zQ5RaDEXH1JgAdMyYOdu3qFL6d+QHyZdVHVeQFilNYE03F6C8AbMWrmj6dBUNlg==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Controls.DataGrid": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Controls.DataGrid": "0.10.11", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "I/++/4Halsx9HIp99nBwB2nIMrI9zw2M8wDcK1HaYVMKU+m3KFA9w+DfV7g/wEceWSMeX7yAvUjRnaUYtBO08Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, - "SkiaSharp.NativeAssets.Linux": { - "type": "Direct", - "requested": "[2.80.3, )", - "resolved": "2.80.3", - "contentHash": "LYl/mvEXrsKMdDNPVjA4ul8JDDGZI8DIkFE0a5GdhaC/aooxgwjuaXZ9NfPg4cJsRf8tb6VhGHvjSNUngNOcJw==", - "dependencies": { - "SkiaSharp": "2.80.3" - } - }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", "resolved": "2.1.0.2020091801", @@ -69,74 +60,76 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "resolved": "0.10.11", + "contentHash": "zvt6QA2uwe18gJ/XdnSMTHG6L/2usvjoaAdPC+Lgg+DmUPNTjqN+Hm1l0AjUtNNId6G+4iIkysiZ2WiHPqGsEA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } }, "Avalonia.Controls.PanAndZoom": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "oUpQm2frhjWll5QWLx8Uzc2VWDNXgPqONlNBLu2gFBis1lkce1jjZvu423U7RNfvg1rOMeZoeiodZq7xZ02STA==", + "resolved": "10.11.1", + "contentHash": "XIjA3iGHMfokPXw/ov5CqKKPR8HdVrTBOMYJVOGpDQyec6RwI/w7lq530wfIMebIe9xUj5RY2Jx5heQtCuAFmg==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "resolved": "0.10.11", + "contentHash": "cj8T11WQ5/opR2IPttb1Bo89aHclkuvHYsCB7HzZU/F7l/cKXbKUOhyo60p44BdFzrCqjNXDnKQbxeRv+OSF7A==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "resolved": "0.10.11", + "contentHash": "9fBC9UArVXEmsxL2Nd0KHGoZUCqcTo06NTlOTAeM3qdEWzE8a0qRVYiR2WeYfADXpKR1D/fQz5zWUZcebFYFIA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + "resolved": "0.10.11", + "contentHash": "kID2N/cXg7KCGFYFTOWCvSLt+oMFRApLfLcbLU35keC/jwDi9tFk33CEdo81hBEg15lAtTtCfvHhNPyVyIYijQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "resolved": "0.10.11", + "contentHash": "4bP5V3BpnZ+If2/ZrZofeRsINeZ6gemLjfNyElt7vNF4HZaRfot03anO3Y+Z7mTELjuol6n/5lAL4+kQUN/O/w==", "dependencies": { - "Avalonia": "0.10.10", - "HarfBuzzSharp": "2.6.1.7", - "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", - "SkiaSharp": "2.80.2", - "SkiaSharp.NativeAssets.Linux": "2.80.2" + "Avalonia": "0.10.11", + "HarfBuzzSharp": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.Linux": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.0-preview.178" } }, "Avalonia.Svg.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "7xQkg3b/djGjGQe6ODxCY+LxMeZ0MtSFUOeEu8IMMUG89gueptuS84GZMRY27xG8lvjP3Mu8B1p3/YY5u7UiDg==", + "resolved": "0.10.11", + "contentHash": "Zw1kfOWN7ZaMqnoJsKqvU/8GxGbrv4KrkAbbLVHvhZl4sA0VZEjqdtxUAqSHlJrYtjPfaUzzDP9K3l0KCqnx/Q==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Skia": "0.10.10", - "SkiaSharp": "2.80.2", - "Svg.Skia": "0.5.10" + "Avalonia": "0.10.11", + "Avalonia.Skia": "0.10.11", + "SkiaSharp": "2.88.0-preview.178", + "Svg.Skia": "0.5.11" } }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "resolved": "0.10.11", + "contentHash": "bckqh8rnQ4+l2kdU4njO3cBKaT4l1HQkxdVYJLAgl44uMtoCpaN7EidrBTnuM40DXa0cpvOh97A+G8jpZgte6Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -144,39 +137,39 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "resolved": "0.10.11", + "contentHash": "joPVaMmPy4bC1STSk5+fAn5zZOT3gz4m/YSv6io3p2q68kEbc+d5KaYk/KcqA/WGiBBQx4a0ViPW/IRomI94kw==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.FreeDesktop": "0.10.10", - "Avalonia.Skia": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.FreeDesktop": "0.10.11", + "Avalonia.Skia": "0.10.11" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "iSVOObfXch8vFzhYFvwGntRt4l4kg+dxXYc4C4RDhnoyPh60BCMUzdk2gYUYySv9UVSGAhqH4dQPWZ0l9USmYA==", + "resolved": "0.10.11.5", + "contentHash": "XHU7/hRYWEdaJOs+weT9ml9/GYqroPrAtePjGzUzUrMoHESbqmkLXmW+fHkaAeXRMJAOAFD1LQUHQu+B6ThF3w==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "OAwrl0tzsz+iX2z3T5wdsEG/JkbnlylfXUjCfbgQlQqODUBO0kk02RpCHJMnV86apkVXjeLV+S5+yA1yX+DT6g==", + "resolved": "0.10.11.5", + "contentHash": "MpqS1t1zypDNEW2Pyg113W4AwmfaWai5LfA/K22sDbygXII+KuACaHt2ZPHJWnvKGHgasLEEFhEOGfF5cB9NPA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "RH33/HboQikon64SCfu1qFpuy4+mox2SEGYbXrrw9mnw3Ogw8PeYTJLtZw8/mNYs6dZAU3NusRBaLIG+0PGisw==", + "resolved": "0.10.11.5", + "contentHash": "MOM6lcPenJZu9LPhai3n67GssBUx3MjoCVLyR2GEJ6lywKQgk4MKOIAC0gLWgy7x1e540oy4lCpeX8jMsqe7mA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Castle.Core": { @@ -219,12 +212,12 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.1.7", - "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", + "resolved": "1.1.8", + "contentHash": "pWxi0zvl4+602rffgZgRIS2srUr/bKFCH/duiV72UodmMp291vaWLC3Lzbp3j5TzSuPHYAlcUBIFvEMlnu8WLQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11" } }, "Flurl": { @@ -244,20 +237,37 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "resolved": "2.8.2-preview.178", + "contentHash": "OUir5qn95QRtlc8RWKfU/63xYwtuAbylL2oAj3eBWgAsVoWnFrEv+Oh1sj0xjW7mogFGaeGtY40lqAD1srWJcQ==", "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2-preview.178", "System.Memory": "4.5.3" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "resolved": "2.8.2-preview.178", + "contentHash": "4scihdELcRpCEubBsUMHUJn93Xvx6ASj596WfO9y8CEuFNW0LBMDL71HBCyq5zXsn8HyGjLtoBLW0PpXbVnpjQ==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7" + "HarfBuzzSharp": "2.8.2-preview.178" } }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "QtmAs62il4vFtt3fFOXhhPDl7TX+NGu4tFB5qmnqUn+EnSJW7mxqNk1n9I7+Z2ORym0nTP4dhcRNtOpOS7Oenw==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "+S8qtBAVTrt+E85jZXPxYthUgSUq7iB6UZ0v0WFsy9gWhZ/hVE3hZJpcgeywT9H/SRX3ZIX+qzpKJlOM+mUcNA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "EgeF5uCZcAriIHmWq3hKNxz/jBJLeP/PKU4yI87UNkJCt4hYignOMjY0irl/rGVZtTL/G05xxf7TB6sjisi8sQ==" + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -529,6 +539,27 @@ "ReactiveUI": "16.2.6" } }, + "RGB.NET.Core": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -667,44 +698,69 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "G1ltdwS5+eMhMCoMx31hHjFIznGAjdUK7xAg8raFMFbN09p2tuxzvK7cKbveWm8SEAGIW7NgDyEqGGJzrPrMrg==" + "resolved": "0.5.11", + "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, "SkiaSharp.HarfBuzz": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "resolved": "2.88.0-preview.178", + "contentHash": "qTf3PbRJsTVOttQkYWOCswa1QnkH0dEOqMSgr3iXZQuKPNlj1qpGY8+OGPs25WKgUEqOpv2nog/AYQ/bpyOXzA==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7", - "SkiaSharp": "2.80.2" + "HarfBuzzSharp": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178" } }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "nc9C8zGvL2G7p0lcTPhN4EOt2Mozv6KLJinMwjF97sYoI5cpkXCPZSRTcyf8k49gAZaOd+UMGaygCAz/8vaaWg==", + "dependencies": { + "SkiaSharp": "2.88.0-preview.178" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "u4Ss81oOlx0dhu5fxl4vK5f2Hm7psHDUSAoQValNV/BmixsW4TkETE3dOnHNRWwI56++tRG9dK33HimZDUrUpw==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "Splat": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" + "resolved": "14.1.17", + "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", + "resolved": "14.1.17", + "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.1" + "Splat": "14.1.17" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Ypi/4NxDxjM24vsK+4Uit08aJUFHcZZpB5Ctb7FLMVevMq0hYeDqzKp6OVLFO5UCX/TxUfRiL1u9F7fvUDA0tQ==", + "resolved": "0.5.11", + "contentHash": "8OEW3UKx07JfEyqzzvF5+ycydusZjg6jsBjDSBrAoq62c5gNZrs6brlOKm2ywEj9hObK3sLcat5BHnE2OUHXsg==", "dependencies": { "Fizzler": "1.2.0", "System.Drawing.Common": "5.0.0", @@ -715,22 +771,22 @@ }, "Svg.Model": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "sh5W7VpghtFjDnPOMa+CEiIKpJPmkb7FxNuHnB2sLZwJR2qzyVlZaBWM95VaRAXlOU8c0qbtA5ZNVREOpzLQRw==", + "resolved": "0.5.11", + "contentHash": "5/Y+BGjgTwobA9aDfcpTGF/bm83MYrEYM8J1LpPohRR+c+B/1N+rbSXfpDZq2omBJ1O0Sa5VjAXw1oAdm1lYLg==", "dependencies": { - "ShimSkiaSharp": "0.5.10", - "Svg.Custom": "0.5.10" + "ShimSkiaSharp": "0.5.11", + "Svg.Custom": "0.5.11" } }, "Svg.Skia": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Xz/nd+5dNJAh7IbfjyduWIJAtpHNqMopX+ORg6ZNnW6l1I7lK20KHlSdMNy9c18vALrA95eU8WFo00nloLVUkQ==", + "resolved": "0.5.11", + "contentHash": "AiN5rSsYBzBUkoh8YK5HoNxRxHtUekp2/6ZAol8qV8oDr6vu8hmPuWjUDwvqCFMi9Dlllc6YsFfvJ1PZCJvYew==", "dependencies": { - "SkiaSharp": "2.80.2", - "SkiaSharp.HarfBuzz": "2.80.2", - "Svg.Custom": "0.5.10", - "Svg.Model": "0.5.10" + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.HarfBuzz": "2.88.0-preview.178", + "Svg.Custom": "0.5.11", + "Svg.Model": "0.5.11" } }, "System.AppContext": { @@ -1662,6 +1718,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", @@ -1686,32 +1745,35 @@ "dependencies": { "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.Controls.PanAndZoom": "4.3.0", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.Controls.PanAndZoom": "10.11.1", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1", - "Splat.Ninject": "14.1.1" + "Splat.Ninject": "14.1.17" } }, "artemis.ui.shared": { "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "Avalonia.Xaml.Behaviors": "0.10.10.4", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "Avalonia.Xaml.Behaviors": "0.10.11.5", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5", + "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index 4e4e770d6..69e1b2744 100644 --- a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -9,11 +9,11 @@ - - + + - - + + diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index e9d390ff7..bb01b4430 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -4,11 +4,11 @@ ".NETCoreApp,Version=v5.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "2PSE+dB4vGJfG+1M+y+Hwaxiqze5mbBTTG9hjwc2Z3U/9yJE/GThBEst2WwI0yBt13hsfAfbABzt1PA3mtbFdw==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,36 +19,36 @@ }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "PQTl4lm7IZidzltMwC7RSNaoz7TYNznU8SKa/WaAI6ycMzC0On2DsqiL1dXr6WhYzMazyMJj6kBhiQzHIc1lIQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Native": "0.10.10", - "Avalonia.Skia": "0.10.10", - "Avalonia.Win32": "0.10.10", - "Avalonia.X11": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Native": "0.10.11", + "Avalonia.Skia": "0.10.11", + "Avalonia.Win32": "0.10.11", + "Avalonia.X11": "0.10.11" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "xBvBkF2DBKjddAfQbExd660zQ5RaDEXH1JgAdMyYOdu3qFL6d+QHyZdVHVeQFilNYE03F6C8AbMWrmj6dBUNlg==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Controls.DataGrid": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Controls.DataGrid": "0.10.11", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "I/++/4Halsx9HIp99nBwB2nIMrI9zw2M8wDcK1HaYVMKU+m3KFA9w+DfV7g/wEceWSMeX7yAvUjRnaUYtBO08Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } @@ -60,74 +60,76 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "resolved": "0.10.11", + "contentHash": "zvt6QA2uwe18gJ/XdnSMTHG6L/2usvjoaAdPC+Lgg+DmUPNTjqN+Hm1l0AjUtNNId6G+4iIkysiZ2WiHPqGsEA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } }, "Avalonia.Controls.PanAndZoom": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "oUpQm2frhjWll5QWLx8Uzc2VWDNXgPqONlNBLu2gFBis1lkce1jjZvu423U7RNfvg1rOMeZoeiodZq7xZ02STA==", + "resolved": "10.11.1", + "contentHash": "XIjA3iGHMfokPXw/ov5CqKKPR8HdVrTBOMYJVOGpDQyec6RwI/w7lq530wfIMebIe9xUj5RY2Jx5heQtCuAFmg==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "resolved": "0.10.11", + "contentHash": "cj8T11WQ5/opR2IPttb1Bo89aHclkuvHYsCB7HzZU/F7l/cKXbKUOhyo60p44BdFzrCqjNXDnKQbxeRv+OSF7A==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "resolved": "0.10.11", + "contentHash": "9fBC9UArVXEmsxL2Nd0KHGoZUCqcTo06NTlOTAeM3qdEWzE8a0qRVYiR2WeYfADXpKR1D/fQz5zWUZcebFYFIA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + "resolved": "0.10.11", + "contentHash": "kID2N/cXg7KCGFYFTOWCvSLt+oMFRApLfLcbLU35keC/jwDi9tFk33CEdo81hBEg15lAtTtCfvHhNPyVyIYijQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "resolved": "0.10.11", + "contentHash": "4bP5V3BpnZ+If2/ZrZofeRsINeZ6gemLjfNyElt7vNF4HZaRfot03anO3Y+Z7mTELjuol6n/5lAL4+kQUN/O/w==", "dependencies": { - "Avalonia": "0.10.10", - "HarfBuzzSharp": "2.6.1.7", - "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", - "SkiaSharp": "2.80.2", - "SkiaSharp.NativeAssets.Linux": "2.80.2" + "Avalonia": "0.10.11", + "HarfBuzzSharp": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.Linux": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.0-preview.178" } }, "Avalonia.Svg.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "7xQkg3b/djGjGQe6ODxCY+LxMeZ0MtSFUOeEu8IMMUG89gueptuS84GZMRY27xG8lvjP3Mu8B1p3/YY5u7UiDg==", + "resolved": "0.10.11", + "contentHash": "Zw1kfOWN7ZaMqnoJsKqvU/8GxGbrv4KrkAbbLVHvhZl4sA0VZEjqdtxUAqSHlJrYtjPfaUzzDP9K3l0KCqnx/Q==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Skia": "0.10.10", - "SkiaSharp": "2.80.2", - "Svg.Skia": "0.5.10" + "Avalonia": "0.10.11", + "Avalonia.Skia": "0.10.11", + "SkiaSharp": "2.88.0-preview.178", + "Svg.Skia": "0.5.11" } }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "resolved": "0.10.11", + "contentHash": "bckqh8rnQ4+l2kdU4njO3cBKaT4l1HQkxdVYJLAgl44uMtoCpaN7EidrBTnuM40DXa0cpvOh97A+G8jpZgte6Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -135,39 +137,39 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "resolved": "0.10.11", + "contentHash": "joPVaMmPy4bC1STSk5+fAn5zZOT3gz4m/YSv6io3p2q68kEbc+d5KaYk/KcqA/WGiBBQx4a0ViPW/IRomI94kw==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.FreeDesktop": "0.10.10", - "Avalonia.Skia": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.FreeDesktop": "0.10.11", + "Avalonia.Skia": "0.10.11" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "iSVOObfXch8vFzhYFvwGntRt4l4kg+dxXYc4C4RDhnoyPh60BCMUzdk2gYUYySv9UVSGAhqH4dQPWZ0l9USmYA==", + "resolved": "0.10.11.5", + "contentHash": "XHU7/hRYWEdaJOs+weT9ml9/GYqroPrAtePjGzUzUrMoHESbqmkLXmW+fHkaAeXRMJAOAFD1LQUHQu+B6ThF3w==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "OAwrl0tzsz+iX2z3T5wdsEG/JkbnlylfXUjCfbgQlQqODUBO0kk02RpCHJMnV86apkVXjeLV+S5+yA1yX+DT6g==", + "resolved": "0.10.11.5", + "contentHash": "MpqS1t1zypDNEW2Pyg113W4AwmfaWai5LfA/K22sDbygXII+KuACaHt2ZPHJWnvKGHgasLEEFhEOGfF5cB9NPA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "RH33/HboQikon64SCfu1qFpuy4+mox2SEGYbXrrw9mnw3Ogw8PeYTJLtZw8/mNYs6dZAU3NusRBaLIG+0PGisw==", + "resolved": "0.10.11.5", + "contentHash": "MOM6lcPenJZu9LPhai3n67GssBUx3MjoCVLyR2GEJ6lywKQgk4MKOIAC0gLWgy7x1e540oy4lCpeX8jMsqe7mA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Castle.Core": { @@ -210,12 +212,12 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.1.7", - "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", + "resolved": "1.1.8", + "contentHash": "pWxi0zvl4+602rffgZgRIS2srUr/bKFCH/duiV72UodmMp291vaWLC3Lzbp3j5TzSuPHYAlcUBIFvEMlnu8WLQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11" } }, "Flurl": { @@ -235,20 +237,37 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "resolved": "2.8.2-preview.178", + "contentHash": "OUir5qn95QRtlc8RWKfU/63xYwtuAbylL2oAj3eBWgAsVoWnFrEv+Oh1sj0xjW7mogFGaeGtY40lqAD1srWJcQ==", "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2-preview.178", "System.Memory": "4.5.3" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "resolved": "2.8.2-preview.178", + "contentHash": "4scihdELcRpCEubBsUMHUJn93Xvx6ASj596WfO9y8CEuFNW0LBMDL71HBCyq5zXsn8HyGjLtoBLW0PpXbVnpjQ==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7" + "HarfBuzzSharp": "2.8.2-preview.178" } }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "QtmAs62il4vFtt3fFOXhhPDl7TX+NGu4tFB5qmnqUn+EnSJW7mxqNk1n9I7+Z2ORym0nTP4dhcRNtOpOS7Oenw==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "+S8qtBAVTrt+E85jZXPxYthUgSUq7iB6UZ0v0WFsy9gWhZ/hVE3hZJpcgeywT9H/SRX3ZIX+qzpKJlOM+mUcNA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "EgeF5uCZcAriIHmWq3hKNxz/jBJLeP/PKU4yI87UNkJCt4hYignOMjY0irl/rGVZtTL/G05xxf7TB6sjisi8sQ==" + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -520,6 +539,27 @@ "ReactiveUI": "16.2.6" } }, + "RGB.NET.Core": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -658,52 +698,69 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "G1ltdwS5+eMhMCoMx31hHjFIznGAjdUK7xAg8raFMFbN09p2tuxzvK7cKbveWm8SEAGIW7NgDyEqGGJzrPrMrg==" + "resolved": "0.5.11", + "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, "SkiaSharp.HarfBuzz": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "resolved": "2.88.0-preview.178", + "contentHash": "qTf3PbRJsTVOttQkYWOCswa1QnkH0dEOqMSgr3iXZQuKPNlj1qpGY8+OGPs25WKgUEqOpv2nog/AYQ/bpyOXzA==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7", - "SkiaSharp": "2.80.2" + "HarfBuzzSharp": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "resolved": "2.88.0-preview.178", + "contentHash": "nc9C8zGvL2G7p0lcTPhN4EOt2Mozv6KLJinMwjF97sYoI5cpkXCPZSRTcyf8k49gAZaOd+UMGaygCAz/8vaaWg==", "dependencies": { - "SkiaSharp": "2.80.2" + "SkiaSharp": "2.88.0-preview.178" } }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "u4Ss81oOlx0dhu5fxl4vK5f2Hm7psHDUSAoQValNV/BmixsW4TkETE3dOnHNRWwI56++tRG9dK33HimZDUrUpw==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "Splat": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" + "resolved": "14.1.17", + "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", + "resolved": "14.1.17", + "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.1" + "Splat": "14.1.17" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Ypi/4NxDxjM24vsK+4Uit08aJUFHcZZpB5Ctb7FLMVevMq0hYeDqzKp6OVLFO5UCX/TxUfRiL1u9F7fvUDA0tQ==", + "resolved": "0.5.11", + "contentHash": "8OEW3UKx07JfEyqzzvF5+ycydusZjg6jsBjDSBrAoq62c5gNZrs6brlOKm2ywEj9hObK3sLcat5BHnE2OUHXsg==", "dependencies": { "Fizzler": "1.2.0", "System.Drawing.Common": "5.0.0", @@ -714,22 +771,22 @@ }, "Svg.Model": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "sh5W7VpghtFjDnPOMa+CEiIKpJPmkb7FxNuHnB2sLZwJR2qzyVlZaBWM95VaRAXlOU8c0qbtA5ZNVREOpzLQRw==", + "resolved": "0.5.11", + "contentHash": "5/Y+BGjgTwobA9aDfcpTGF/bm83MYrEYM8J1LpPohRR+c+B/1N+rbSXfpDZq2omBJ1O0Sa5VjAXw1oAdm1lYLg==", "dependencies": { - "ShimSkiaSharp": "0.5.10", - "Svg.Custom": "0.5.10" + "ShimSkiaSharp": "0.5.11", + "Svg.Custom": "0.5.11" } }, "Svg.Skia": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Xz/nd+5dNJAh7IbfjyduWIJAtpHNqMopX+ORg6ZNnW6l1I7lK20KHlSdMNy9c18vALrA95eU8WFo00nloLVUkQ==", + "resolved": "0.5.11", + "contentHash": "AiN5rSsYBzBUkoh8YK5HoNxRxHtUekp2/6ZAol8qV8oDr6vu8hmPuWjUDwvqCFMi9Dlllc6YsFfvJ1PZCJvYew==", "dependencies": { - "SkiaSharp": "2.80.2", - "SkiaSharp.HarfBuzz": "2.80.2", - "Svg.Custom": "0.5.10", - "Svg.Model": "0.5.10" + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.HarfBuzz": "2.88.0-preview.178", + "Svg.Custom": "0.5.11", + "Svg.Model": "0.5.11" } }, "System.AppContext": { @@ -1661,6 +1718,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", @@ -1685,32 +1745,35 @@ "dependencies": { "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.Controls.PanAndZoom": "4.3.0", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.Controls.PanAndZoom": "10.11.1", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1", - "Splat.Ninject": "14.1.1" + "Splat.Ninject": "14.1.17" } }, "artemis.ui.shared": { "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "Avalonia.Xaml.Behaviors": "0.10.10.4", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "Avalonia.Xaml.Behaviors": "0.10.11.5", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5", + "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 63cb328e3..8d084dfdd 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -16,15 +16,16 @@ - - - - - - - + + + + + + + + @@ -39,9 +40,6 @@ ..\..\..\..\Users\Robert\.nuget\packages\material.icons.avalonia\1.0.2\lib\netstandard2.0\Material.Icons.Avalonia.dll - - ..\..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index 702356de1..dd243ccf3 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -4,11 +4,11 @@ ".NETCoreApp,Version=v5.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "2PSE+dB4vGJfG+1M+y+Hwaxiqze5mbBTTG9hjwc2Z3U/9yJE/GThBEst2WwI0yBt13hsfAfbABzt1PA3mtbFdw==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,66 +19,66 @@ }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "I/++/4Halsx9HIp99nBwB2nIMrI9zw2M8wDcK1HaYVMKU+m3KFA9w+DfV7g/wEceWSMeX7yAvUjRnaUYtBO08Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, "Avalonia.Svg.Skia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "7xQkg3b/djGjGQe6ODxCY+LxMeZ0MtSFUOeEu8IMMUG89gueptuS84GZMRY27xG8lvjP3Mu8B1p3/YY5u7UiDg==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "Zw1kfOWN7ZaMqnoJsKqvU/8GxGbrv4KrkAbbLVHvhZl4sA0VZEjqdtxUAqSHlJrYtjPfaUzzDP9K3l0KCqnx/Q==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Skia": "0.10.10", - "SkiaSharp": "2.80.2", - "Svg.Skia": "0.5.10" + "Avalonia": "0.10.11", + "Avalonia.Skia": "0.10.11", + "SkiaSharp": "2.88.0-preview.178", + "Svg.Skia": "0.5.11" } }, "Avalonia.Xaml.Behaviors": { "type": "Direct", - "requested": "[0.10.10.4, )", - "resolved": "0.10.10.4", - "contentHash": "iSVOObfXch8vFzhYFvwGntRt4l4kg+dxXYc4C4RDhnoyPh60BCMUzdk2gYUYySv9UVSGAhqH4dQPWZ0l9USmYA==", + "requested": "[0.10.11.5, )", + "resolved": "0.10.11.5", + "contentHash": "XHU7/hRYWEdaJOs+weT9ml9/GYqroPrAtePjGzUzUrMoHESbqmkLXmW+fHkaAeXRMJAOAFD1LQUHQu+B6ThF3w==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactions": { "type": "Direct", - "requested": "[0.10.10.4, )", - "resolved": "0.10.10.4", - "contentHash": "OAwrl0tzsz+iX2z3T5wdsEG/JkbnlylfXUjCfbgQlQqODUBO0kk02RpCHJMnV86apkVXjeLV+S5+yA1yX+DT6g==", + "requested": "[0.10.11.5, )", + "resolved": "0.10.11.5", + "contentHash": "MpqS1t1zypDNEW2Pyg113W4AwmfaWai5LfA/K22sDbygXII+KuACaHt2ZPHJWnvKGHgasLEEFhEOGfF5cB9NPA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactivity": { "type": "Direct", - "requested": "[0.10.10.4, )", - "resolved": "0.10.10.4", - "contentHash": "RH33/HboQikon64SCfu1qFpuy4+mox2SEGYbXrrw9mnw3Ogw8PeYTJLtZw8/mNYs6dZAU3NusRBaLIG+0PGisw==", + "requested": "[0.10.11.5, )", + "resolved": "0.10.11.5", + "contentHash": "MOM6lcPenJZu9LPhai3n67GssBUx3MjoCVLyR2GEJ6lywKQgk4MKOIAC0gLWgy7x1e540oy4lCpeX8jMsqe7mA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.1.7, )", - "resolved": "1.1.7", - "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", + "requested": "[1.1.8, )", + "resolved": "1.1.8", + "contentHash": "pWxi0zvl4+602rffgZgRIS2srUr/bKFCH/duiV72UodmMp291vaWLC3Lzbp3j5TzSuPHYAlcUBIFvEMlnu8WLQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11" } }, "Material.Icons.Avalonia": { @@ -100,6 +100,12 @@ "ReactiveUI": "16.2.6" } }, + "RGB.NET.Core": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", "resolved": "2.1.0.2020091801", @@ -107,78 +113,80 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "resolved": "0.10.11", + "contentHash": "zvt6QA2uwe18gJ/XdnSMTHG6L/2usvjoaAdPC+Lgg+DmUPNTjqN+Hm1l0AjUtNNId6G+4iIkysiZ2WiHPqGsEA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } }, "Avalonia.Desktop": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "resolved": "0.10.11", + "contentHash": "PQTl4lm7IZidzltMwC7RSNaoz7TYNznU8SKa/WaAI6ycMzC0On2DsqiL1dXr6WhYzMazyMJj6kBhiQzHIc1lIQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Native": "0.10.10", - "Avalonia.Skia": "0.10.10", - "Avalonia.Win32": "0.10.10", - "Avalonia.X11": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Native": "0.10.11", + "Avalonia.Skia": "0.10.11", + "Avalonia.Win32": "0.10.11", + "Avalonia.X11": "0.10.11" } }, "Avalonia.Diagnostics": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "resolved": "0.10.11", + "contentHash": "xBvBkF2DBKjddAfQbExd660zQ5RaDEXH1JgAdMyYOdu3qFL6d+QHyZdVHVeQFilNYE03F6C8AbMWrmj6dBUNlg==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Controls.DataGrid": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Controls.DataGrid": "0.10.11", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "resolved": "0.10.11", + "contentHash": "cj8T11WQ5/opR2IPttb1Bo89aHclkuvHYsCB7HzZU/F7l/cKXbKUOhyo60p44BdFzrCqjNXDnKQbxeRv+OSF7A==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "resolved": "0.10.11", + "contentHash": "9fBC9UArVXEmsxL2Nd0KHGoZUCqcTo06NTlOTAeM3qdEWzE8a0qRVYiR2WeYfADXpKR1D/fQz5zWUZcebFYFIA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + "resolved": "0.10.11", + "contentHash": "kID2N/cXg7KCGFYFTOWCvSLt+oMFRApLfLcbLU35keC/jwDi9tFk33CEdo81hBEg15lAtTtCfvHhNPyVyIYijQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "resolved": "0.10.11", + "contentHash": "4bP5V3BpnZ+If2/ZrZofeRsINeZ6gemLjfNyElt7vNF4HZaRfot03anO3Y+Z7mTELjuol6n/5lAL4+kQUN/O/w==", "dependencies": { - "Avalonia": "0.10.10", - "HarfBuzzSharp": "2.6.1.7", - "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", - "SkiaSharp": "2.80.2", - "SkiaSharp.NativeAssets.Linux": "2.80.2" + "Avalonia": "0.10.11", + "HarfBuzzSharp": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.Linux": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.0-preview.178" } }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "resolved": "0.10.11", + "contentHash": "bckqh8rnQ4+l2kdU4njO3cBKaT4l1HQkxdVYJLAgl44uMtoCpaN7EidrBTnuM40DXa0cpvOh97A+G8jpZgte6Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -186,12 +194,12 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "resolved": "0.10.11", + "contentHash": "joPVaMmPy4bC1STSk5+fAn5zZOT3gz4m/YSv6io3p2q68kEbc+d5KaYk/KcqA/WGiBBQx4a0ViPW/IRomI94kw==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.FreeDesktop": "0.10.10", - "Avalonia.Skia": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.FreeDesktop": "0.10.11", + "Avalonia.Skia": "0.10.11" } }, "Castle.Core": { @@ -234,20 +242,37 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "resolved": "2.8.2-preview.178", + "contentHash": "OUir5qn95QRtlc8RWKfU/63xYwtuAbylL2oAj3eBWgAsVoWnFrEv+Oh1sj0xjW7mogFGaeGtY40lqAD1srWJcQ==", "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2-preview.178", "System.Memory": "4.5.3" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "resolved": "2.8.2-preview.178", + "contentHash": "4scihdELcRpCEubBsUMHUJn93Xvx6ASj596WfO9y8CEuFNW0LBMDL71HBCyq5zXsn8HyGjLtoBLW0PpXbVnpjQ==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7" + "HarfBuzzSharp": "2.8.2-preview.178" } }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "QtmAs62il4vFtt3fFOXhhPDl7TX+NGu4tFB5qmnqUn+EnSJW7mxqNk1n9I7+Z2ORym0nTP4dhcRNtOpOS7Oenw==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "+S8qtBAVTrt+E85jZXPxYthUgSUq7iB6UZ0v0WFsy9gWhZ/hVE3hZJpcgeywT9H/SRX3ZIX+qzpKJlOM+mUcNA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "EgeF5uCZcAriIHmWq3hKNxz/jBJLeP/PKU4yI87UNkJCt4hYignOMjY0irl/rGVZtTL/G05xxf7TB6sjisi8sQ==" + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -494,6 +519,22 @@ "Splat": "13.1.1" } }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -632,34 +673,51 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "G1ltdwS5+eMhMCoMx31hHjFIznGAjdUK7xAg8raFMFbN09p2tuxzvK7cKbveWm8SEAGIW7NgDyEqGGJzrPrMrg==" + "resolved": "0.5.11", + "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, "SkiaSharp.HarfBuzz": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "resolved": "2.88.0-preview.178", + "contentHash": "qTf3PbRJsTVOttQkYWOCswa1QnkH0dEOqMSgr3iXZQuKPNlj1qpGY8+OGPs25WKgUEqOpv2nog/AYQ/bpyOXzA==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7", - "SkiaSharp": "2.80.2" + "HarfBuzzSharp": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "resolved": "2.88.0-preview.178", + "contentHash": "nc9C8zGvL2G7p0lcTPhN4EOt2Mozv6KLJinMwjF97sYoI5cpkXCPZSRTcyf8k49gAZaOd+UMGaygCAz/8vaaWg==", "dependencies": { - "SkiaSharp": "2.80.2" + "SkiaSharp": "2.88.0-preview.178" } }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "u4Ss81oOlx0dhu5fxl4vK5f2Hm7psHDUSAoQValNV/BmixsW4TkETE3dOnHNRWwI56++tRG9dK33HimZDUrUpw==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "Splat": { "type": "Transitive", "resolved": "13.1.1", @@ -667,8 +725,8 @@ }, "Svg.Custom": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Ypi/4NxDxjM24vsK+4Uit08aJUFHcZZpB5Ctb7FLMVevMq0hYeDqzKp6OVLFO5UCX/TxUfRiL1u9F7fvUDA0tQ==", + "resolved": "0.5.11", + "contentHash": "8OEW3UKx07JfEyqzzvF5+ycydusZjg6jsBjDSBrAoq62c5gNZrs6brlOKm2ywEj9hObK3sLcat5BHnE2OUHXsg==", "dependencies": { "Fizzler": "1.2.0", "System.Drawing.Common": "5.0.0", @@ -679,22 +737,22 @@ }, "Svg.Model": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "sh5W7VpghtFjDnPOMa+CEiIKpJPmkb7FxNuHnB2sLZwJR2qzyVlZaBWM95VaRAXlOU8c0qbtA5ZNVREOpzLQRw==", + "resolved": "0.5.11", + "contentHash": "5/Y+BGjgTwobA9aDfcpTGF/bm83MYrEYM8J1LpPohRR+c+B/1N+rbSXfpDZq2omBJ1O0Sa5VjAXw1oAdm1lYLg==", "dependencies": { - "ShimSkiaSharp": "0.5.10", - "Svg.Custom": "0.5.10" + "ShimSkiaSharp": "0.5.11", + "Svg.Custom": "0.5.11" } }, "Svg.Skia": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Xz/nd+5dNJAh7IbfjyduWIJAtpHNqMopX+ORg6ZNnW6l1I7lK20KHlSdMNy9c18vALrA95eU8WFo00nloLVUkQ==", + "resolved": "0.5.11", + "contentHash": "AiN5rSsYBzBUkoh8YK5HoNxRxHtUekp2/6ZAol8qV8oDr6vu8hmPuWjUDwvqCFMi9Dlllc6YsFfvJ1PZCJvYew==", "dependencies": { - "SkiaSharp": "2.80.2", - "SkiaSharp.HarfBuzz": "2.80.2", - "Svg.Custom": "0.5.10", - "Svg.Model": "0.5.10" + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.HarfBuzz": "2.88.0-preview.178", + "Svg.Custom": "0.5.11", + "Svg.Model": "0.5.11" } }, "System.AppContext": { @@ -1626,6 +1684,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 3e6be8758..427ae971b 100644 --- a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -9,12 +9,12 @@ - - + + - - - + + + diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index 8ac82b3ba..572a80758 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -4,11 +4,11 @@ "net5.0-windows7.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "2PSE+dB4vGJfG+1M+y+Hwaxiqze5mbBTTG9hjwc2Z3U/9yJE/GThBEst2WwI0yBt13hsfAfbABzt1PA3mtbFdw==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,47 +19,47 @@ }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "PQTl4lm7IZidzltMwC7RSNaoz7TYNznU8SKa/WaAI6ycMzC0On2DsqiL1dXr6WhYzMazyMJj6kBhiQzHIc1lIQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Native": "0.10.10", - "Avalonia.Skia": "0.10.10", - "Avalonia.Win32": "0.10.10", - "Avalonia.X11": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Native": "0.10.11", + "Avalonia.Skia": "0.10.11", + "Avalonia.Win32": "0.10.11", + "Avalonia.X11": "0.10.11" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "xBvBkF2DBKjddAfQbExd660zQ5RaDEXH1JgAdMyYOdu3qFL6d+QHyZdVHVeQFilNYE03F6C8AbMWrmj6dBUNlg==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Controls.DataGrid": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Controls.DataGrid": "0.10.11", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "I/++/4Halsx9HIp99nBwB2nIMrI9zw2M8wDcK1HaYVMKU+m3KFA9w+DfV7g/wEceWSMeX7yAvUjRnaUYtBO08Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, "Avalonia.Win32": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "bckqh8rnQ4+l2kdU4njO3cBKaT4l1HQkxdVYJLAgl44uMtoCpaN7EidrBTnuM40DXa0cpvOh97A+G8jpZgte6Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -87,103 +87,105 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "resolved": "0.10.11", + "contentHash": "zvt6QA2uwe18gJ/XdnSMTHG6L/2usvjoaAdPC+Lgg+DmUPNTjqN+Hm1l0AjUtNNId6G+4iIkysiZ2WiHPqGsEA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } }, "Avalonia.Controls.PanAndZoom": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "oUpQm2frhjWll5QWLx8Uzc2VWDNXgPqONlNBLu2gFBis1lkce1jjZvu423U7RNfvg1rOMeZoeiodZq7xZ02STA==", + "resolved": "10.11.1", + "contentHash": "XIjA3iGHMfokPXw/ov5CqKKPR8HdVrTBOMYJVOGpDQyec6RwI/w7lq530wfIMebIe9xUj5RY2Jx5heQtCuAFmg==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "resolved": "0.10.11", + "contentHash": "cj8T11WQ5/opR2IPttb1Bo89aHclkuvHYsCB7HzZU/F7l/cKXbKUOhyo60p44BdFzrCqjNXDnKQbxeRv+OSF7A==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "resolved": "0.10.11", + "contentHash": "9fBC9UArVXEmsxL2Nd0KHGoZUCqcTo06NTlOTAeM3qdEWzE8a0qRVYiR2WeYfADXpKR1D/fQz5zWUZcebFYFIA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + "resolved": "0.10.11", + "contentHash": "kID2N/cXg7KCGFYFTOWCvSLt+oMFRApLfLcbLU35keC/jwDi9tFk33CEdo81hBEg15lAtTtCfvHhNPyVyIYijQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "resolved": "0.10.11", + "contentHash": "4bP5V3BpnZ+If2/ZrZofeRsINeZ6gemLjfNyElt7vNF4HZaRfot03anO3Y+Z7mTELjuol6n/5lAL4+kQUN/O/w==", "dependencies": { - "Avalonia": "0.10.10", - "HarfBuzzSharp": "2.6.1.7", - "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", - "SkiaSharp": "2.80.2", - "SkiaSharp.NativeAssets.Linux": "2.80.2" + "Avalonia": "0.10.11", + "HarfBuzzSharp": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.Linux": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.0-preview.178" } }, "Avalonia.Svg.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "7xQkg3b/djGjGQe6ODxCY+LxMeZ0MtSFUOeEu8IMMUG89gueptuS84GZMRY27xG8lvjP3Mu8B1p3/YY5u7UiDg==", + "resolved": "0.10.11", + "contentHash": "Zw1kfOWN7ZaMqnoJsKqvU/8GxGbrv4KrkAbbLVHvhZl4sA0VZEjqdtxUAqSHlJrYtjPfaUzzDP9K3l0KCqnx/Q==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Skia": "0.10.10", - "SkiaSharp": "2.80.2", - "Svg.Skia": "0.5.10" + "Avalonia": "0.10.11", + "Avalonia.Skia": "0.10.11", + "SkiaSharp": "2.88.0-preview.178", + "Svg.Skia": "0.5.11" } }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "resolved": "0.10.11", + "contentHash": "joPVaMmPy4bC1STSk5+fAn5zZOT3gz4m/YSv6io3p2q68kEbc+d5KaYk/KcqA/WGiBBQx4a0ViPW/IRomI94kw==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.FreeDesktop": "0.10.10", - "Avalonia.Skia": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.FreeDesktop": "0.10.11", + "Avalonia.Skia": "0.10.11" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "iSVOObfXch8vFzhYFvwGntRt4l4kg+dxXYc4C4RDhnoyPh60BCMUzdk2gYUYySv9UVSGAhqH4dQPWZ0l9USmYA==", + "resolved": "0.10.11.5", + "contentHash": "XHU7/hRYWEdaJOs+weT9ml9/GYqroPrAtePjGzUzUrMoHESbqmkLXmW+fHkaAeXRMJAOAFD1LQUHQu+B6ThF3w==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "OAwrl0tzsz+iX2z3T5wdsEG/JkbnlylfXUjCfbgQlQqODUBO0kk02RpCHJMnV86apkVXjeLV+S5+yA1yX+DT6g==", + "resolved": "0.10.11.5", + "contentHash": "MpqS1t1zypDNEW2Pyg113W4AwmfaWai5LfA/K22sDbygXII+KuACaHt2ZPHJWnvKGHgasLEEFhEOGfF5cB9NPA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "RH33/HboQikon64SCfu1qFpuy4+mox2SEGYbXrrw9mnw3Ogw8PeYTJLtZw8/mNYs6dZAU3NusRBaLIG+0PGisw==", + "resolved": "0.10.11.5", + "contentHash": "MOM6lcPenJZu9LPhai3n67GssBUx3MjoCVLyR2GEJ6lywKQgk4MKOIAC0gLWgy7x1e540oy4lCpeX8jMsqe7mA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Castle.Core": { @@ -226,12 +228,12 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.1.7", - "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", + "resolved": "1.1.8", + "contentHash": "pWxi0zvl4+602rffgZgRIS2srUr/bKFCH/duiV72UodmMp291vaWLC3Lzbp3j5TzSuPHYAlcUBIFvEMlnu8WLQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11" } }, "Flurl": { @@ -251,20 +253,37 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "resolved": "2.8.2-preview.178", + "contentHash": "OUir5qn95QRtlc8RWKfU/63xYwtuAbylL2oAj3eBWgAsVoWnFrEv+Oh1sj0xjW7mogFGaeGtY40lqAD1srWJcQ==", "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2-preview.178", "System.Memory": "4.5.3" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "resolved": "2.8.2-preview.178", + "contentHash": "4scihdELcRpCEubBsUMHUJn93Xvx6ASj596WfO9y8CEuFNW0LBMDL71HBCyq5zXsn8HyGjLtoBLW0PpXbVnpjQ==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7" + "HarfBuzzSharp": "2.8.2-preview.178" } }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "QtmAs62il4vFtt3fFOXhhPDl7TX+NGu4tFB5qmnqUn+EnSJW7mxqNk1n9I7+Z2ORym0nTP4dhcRNtOpOS7Oenw==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "+S8qtBAVTrt+E85jZXPxYthUgSUq7iB6UZ0v0WFsy9gWhZ/hVE3hZJpcgeywT9H/SRX3ZIX+qzpKJlOM+mUcNA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "EgeF5uCZcAriIHmWq3hKNxz/jBJLeP/PKU4yI87UNkJCt4hYignOMjY0irl/rGVZtTL/G05xxf7TB6sjisi8sQ==" + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -536,6 +555,27 @@ "ReactiveUI": "16.2.6" } }, + "RGB.NET.Core": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -674,52 +714,69 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "G1ltdwS5+eMhMCoMx31hHjFIznGAjdUK7xAg8raFMFbN09p2tuxzvK7cKbveWm8SEAGIW7NgDyEqGGJzrPrMrg==" + "resolved": "0.5.11", + "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, "SkiaSharp.HarfBuzz": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "resolved": "2.88.0-preview.178", + "contentHash": "qTf3PbRJsTVOttQkYWOCswa1QnkH0dEOqMSgr3iXZQuKPNlj1qpGY8+OGPs25WKgUEqOpv2nog/AYQ/bpyOXzA==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7", - "SkiaSharp": "2.80.2" + "HarfBuzzSharp": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "resolved": "2.88.0-preview.178", + "contentHash": "nc9C8zGvL2G7p0lcTPhN4EOt2Mozv6KLJinMwjF97sYoI5cpkXCPZSRTcyf8k49gAZaOd+UMGaygCAz/8vaaWg==", "dependencies": { - "SkiaSharp": "2.80.2" + "SkiaSharp": "2.88.0-preview.178" } }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "u4Ss81oOlx0dhu5fxl4vK5f2Hm7psHDUSAoQValNV/BmixsW4TkETE3dOnHNRWwI56++tRG9dK33HimZDUrUpw==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "Splat": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" + "resolved": "14.1.17", + "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", + "resolved": "14.1.17", + "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.1" + "Splat": "14.1.17" } }, "Svg.Custom": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Ypi/4NxDxjM24vsK+4Uit08aJUFHcZZpB5Ctb7FLMVevMq0hYeDqzKp6OVLFO5UCX/TxUfRiL1u9F7fvUDA0tQ==", + "resolved": "0.5.11", + "contentHash": "8OEW3UKx07JfEyqzzvF5+ycydusZjg6jsBjDSBrAoq62c5gNZrs6brlOKm2ywEj9hObK3sLcat5BHnE2OUHXsg==", "dependencies": { "Fizzler": "1.2.0", "System.Drawing.Common": "5.0.0", @@ -730,22 +787,22 @@ }, "Svg.Model": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "sh5W7VpghtFjDnPOMa+CEiIKpJPmkb7FxNuHnB2sLZwJR2qzyVlZaBWM95VaRAXlOU8c0qbtA5ZNVREOpzLQRw==", + "resolved": "0.5.11", + "contentHash": "5/Y+BGjgTwobA9aDfcpTGF/bm83MYrEYM8J1LpPohRR+c+B/1N+rbSXfpDZq2omBJ1O0Sa5VjAXw1oAdm1lYLg==", "dependencies": { - "ShimSkiaSharp": "0.5.10", - "Svg.Custom": "0.5.10" + "ShimSkiaSharp": "0.5.11", + "Svg.Custom": "0.5.11" } }, "Svg.Skia": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Xz/nd+5dNJAh7IbfjyduWIJAtpHNqMopX+ORg6ZNnW6l1I7lK20KHlSdMNy9c18vALrA95eU8WFo00nloLVUkQ==", + "resolved": "0.5.11", + "contentHash": "AiN5rSsYBzBUkoh8YK5HoNxRxHtUekp2/6ZAol8qV8oDr6vu8hmPuWjUDwvqCFMi9Dlllc6YsFfvJ1PZCJvYew==", "dependencies": { - "SkiaSharp": "2.80.2", - "SkiaSharp.HarfBuzz": "2.80.2", - "Svg.Custom": "0.5.10", - "Svg.Model": "0.5.10" + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.HarfBuzz": "2.88.0-preview.178", + "Svg.Custom": "0.5.11", + "Svg.Model": "0.5.11" } }, "System.AppContext": { @@ -1677,6 +1734,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", @@ -1701,32 +1761,35 @@ "dependencies": { "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.Controls.PanAndZoom": "4.3.0", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.Controls.PanAndZoom": "10.11.1", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1", - "Splat.Ninject": "14.1.1" + "Splat.Ninject": "14.1.17" } }, "artemis.ui.shared": { "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "Avalonia.Xaml.Behaviors": "0.10.10.4", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "Avalonia.Xaml.Behaviors": "0.10.11.5", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5", + "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 8f59f87df..2c86fecb9 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -13,28 +13,25 @@ - - - - - - - + + + + + + + - + + + - - - ..\..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml.cs b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs index f1e173403..84f57d048 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml.cs +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml.cs @@ -20,9 +20,9 @@ namespace Artemis.UI private void SetupTitlebar() { - object? titleBar = this.FindResource("RootWindowTitlebar"); - if (titleBar != null) - SetTitleBar((IControl) titleBar); + //object? titleBar = this.FindResource("RootWindowTitlebar"); + //if (titleBar != null) + // SetTitleBar((IControl) titleBar); } private void InitializeComponent() diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index 627d2a480..908975a90 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -4,11 +4,11 @@ ".NETCoreApp,Version=v5.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "wHkEiuUKDNbgzR6VOAMmKcvunRnAX2CpZeZHTjMUqvTHEHaBFsqpinabmQ2ABtxBkdQF7Lyv6AgoS6dlM9eowQ==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "2PSE+dB4vGJfG+1M+y+Hwaxiqze5mbBTTG9hjwc2Z3U/9yJE/GThBEst2WwI0yBt13hsfAfbABzt1PA3mtbFdw==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,70 +19,70 @@ }, "Avalonia.Controls.PanAndZoom": { "type": "Direct", - "requested": "[4.3.0, )", - "resolved": "4.3.0", - "contentHash": "oUpQm2frhjWll5QWLx8Uzc2VWDNXgPqONlNBLu2gFBis1lkce1jjZvu423U7RNfvg1rOMeZoeiodZq7xZ02STA==", + "requested": "[10.11.1, )", + "resolved": "10.11.1", + "contentHash": "XIjA3iGHMfokPXw/ov5CqKKPR8HdVrTBOMYJVOGpDQyec6RwI/w7lq530wfIMebIe9xUj5RY2Jx5heQtCuAFmg==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "K23aC2UxplUqbKvSehgcwLRU0dACRLSQGLs3bXKKW1n6ICXtWhwqSmx8a1Ju0PbbQISRfoc0IjHoAXlGRNZ1dA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "PQTl4lm7IZidzltMwC7RSNaoz7TYNznU8SKa/WaAI6ycMzC0On2DsqiL1dXr6WhYzMazyMJj6kBhiQzHIc1lIQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Native": "0.10.10", - "Avalonia.Skia": "0.10.10", - "Avalonia.Win32": "0.10.10", - "Avalonia.X11": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Native": "0.10.11", + "Avalonia.Skia": "0.10.11", + "Avalonia.Win32": "0.10.11", + "Avalonia.X11": "0.10.11" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "k4VA+uch7Xtd6kqp+A6XEpsVuARseIh6PQtarI3lxcTFFrNbxDZhD1nXUILXrnp44uQ7JPGpKYGlJ0EElfxhbA==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "xBvBkF2DBKjddAfQbExd660zQ5RaDEXH1JgAdMyYOdu3qFL6d+QHyZdVHVeQFilNYE03F6C8AbMWrmj6dBUNlg==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Controls.DataGrid": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Controls.DataGrid": "0.10.11", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "I/++/4Halsx9HIp99nBwB2nIMrI9zw2M8wDcK1HaYVMKU+m3KFA9w+DfV7g/wEceWSMeX7yAvUjRnaUYtBO08Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, "Avalonia.Svg.Skia": { "type": "Direct", - "requested": "[0.10.10, )", - "resolved": "0.10.10", - "contentHash": "7xQkg3b/djGjGQe6ODxCY+LxMeZ0MtSFUOeEu8IMMUG89gueptuS84GZMRY27xG8lvjP3Mu8B1p3/YY5u7UiDg==", + "requested": "[0.10.11, )", + "resolved": "0.10.11", + "contentHash": "Zw1kfOWN7ZaMqnoJsKqvU/8GxGbrv4KrkAbbLVHvhZl4sA0VZEjqdtxUAqSHlJrYtjPfaUzzDP9K3l0KCqnx/Q==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Skia": "0.10.10", - "SkiaSharp": "2.80.2", - "Svg.Skia": "0.5.10" + "Avalonia": "0.10.11", + "Avalonia.Skia": "0.10.11", + "SkiaSharp": "2.88.0-preview.178", + "Svg.Skia": "0.5.11" } }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.1.7, )", - "resolved": "1.1.7", - "contentHash": "Hco3+M09A7rDknaYu5CcyGhUOqGj5mebrFzjZVGjW5YBYiMiV+x0vl2Fn6oIfXxhKgg1i70tTwQCtW/1GWFJeA==", + "requested": "[1.1.8, )", + "resolved": "1.1.8", + "contentHash": "pWxi0zvl4+602rffgZgRIS2srUr/bKFCH/duiV72UodmMp291vaWLC3Lzbp3j5TzSuPHYAlcUBIFvEMlnu8WLQ==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Desktop": "0.10.10", - "Avalonia.Diagnostics": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.Desktop": "0.10.11", + "Avalonia.Diagnostics": "0.10.11" } }, "Flurl.Http": { @@ -124,14 +124,29 @@ "ReactiveUI": "16.2.6" } }, + "RGB.NET.Core": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Direct", + "requested": "[1.0.0-prerelease1, )", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "Splat.Ninject": { "type": "Direct", - "requested": "[14.1.1, )", - "resolved": "14.1.1", - "contentHash": "lmhjY5yRKL6SgkwY0kEtnHI8JERYhRwFiOk+NgvOUtGYzVm/RRm1OrgrN+kPKzmuvsUrodx6FYTHrSkHL2I8Yg==", + "requested": "[14.1.17, )", + "resolved": "14.1.17", + "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.1" + "Splat": "14.1.17" } }, "Avalonia.Angle.Windows.Natives": { @@ -141,55 +156,57 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "AsKm4xBJuCnIdUibNnsU5mNd6+kivhO5gEmpzO9+kNvVZCXxJkKZfmqS+9ghqXnF5c4BDYF5BPvPjZ1cP/jn7Q==", + "resolved": "0.10.11", + "contentHash": "zvt6QA2uwe18gJ/XdnSMTHG6L/2usvjoaAdPC+Lgg+DmUPNTjqN+Hm1l0AjUtNNId6G+4iIkysiZ2WiHPqGsEA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Remote.Protocol": "0.10.10", + "Avalonia": "0.10.11", + "Avalonia.Remote.Protocol": "0.10.11", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pflbsb3CQkZH6T7NCG16Cu/LhA0kJD2ZvRprjzueIWonuS4pxF231Z2T3xv5LGaXpN44ufBjpMvlczCmb6sieQ==", + "resolved": "0.10.11", + "contentHash": "cj8T11WQ5/opR2IPttb1Bo89aHclkuvHYsCB7HzZU/F7l/cKXbKUOhyo60p44BdFzrCqjNXDnKQbxeRv+OSF7A==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "pJ8mlzjtlhPA7ueHnCN4FjBmXZMXJ+hKG+6uLnz+3A879oGLei6yacYRVel80sVoIML1ir8A5InWL52ra1Qdag==", + "resolved": "0.10.11", + "contentHash": "9fBC9UArVXEmsxL2Nd0KHGoZUCqcTo06NTlOTAeM3qdEWzE8a0qRVYiR2WeYfADXpKR1D/fQz5zWUZcebFYFIA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "ZGxDGtIj4SU361ILVBQFd4kqimya7x+aris3CRCzbJuwUXl6bRlQa8MVqsCVx1y1wwnkhteCAS2IEnHHQ/Vghw==" + "resolved": "0.10.11", + "contentHash": "kID2N/cXg7KCGFYFTOWCvSLt+oMFRApLfLcbLU35keC/jwDi9tFk33CEdo81hBEg15lAtTtCfvHhNPyVyIYijQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "8KtlObMQ+8pDMch6SMdPNpIWk9J0OaPjA7lbALEsDkRNb+XLDdIZXWbKle5Y6ASUEQhQGIX4DCP/8UYp7Us5zg==", + "resolved": "0.10.11", + "contentHash": "4bP5V3BpnZ+If2/ZrZofeRsINeZ6gemLjfNyElt7vNF4HZaRfot03anO3Y+Z7mTELjuol6n/5lAL4+kQUN/O/w==", "dependencies": { - "Avalonia": "0.10.10", - "HarfBuzzSharp": "2.6.1.7", - "HarfBuzzSharp.NativeAssets.Linux": "2.6.1.7", - "SkiaSharp": "2.80.2", - "SkiaSharp.NativeAssets.Linux": "2.80.2" + "Avalonia": "0.10.11", + "HarfBuzzSharp": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.Linux": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.0-preview.178" } }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "6AS6yIB+OS8+g96mj+ShJihjxqhVH6v7jfdqLwjQfGAsqqqN7zBNsFdvoVVCnutuVx0g/9FhCnBTIZyZDlwqkA==", + "resolved": "0.10.11", + "contentHash": "bckqh8rnQ4+l2kdU4njO3cBKaT4l1HQkxdVYJLAgl44uMtoCpaN7EidrBTnuM40DXa0cpvOh97A+G8jpZgte6Q==", "dependencies": { - "Avalonia": "0.10.10", + "Avalonia": "0.10.11", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -197,39 +214,39 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.10", - "contentHash": "XsWWNYlKy3XJ8HFzCvv/2Ym8Ku72tN+JxbPX8lLBZSYzQEtvfKQ+DcKb8us1AWjXQhQQSrZQylrtVZ043a4SsQ==", + "resolved": "0.10.11", + "contentHash": "joPVaMmPy4bC1STSk5+fAn5zZOT3gz4m/YSv6io3p2q68kEbc+d5KaYk/KcqA/WGiBBQx4a0ViPW/IRomI94kw==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.FreeDesktop": "0.10.10", - "Avalonia.Skia": "0.10.10" + "Avalonia": "0.10.11", + "Avalonia.FreeDesktop": "0.10.11", + "Avalonia.Skia": "0.10.11" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "iSVOObfXch8vFzhYFvwGntRt4l4kg+dxXYc4C4RDhnoyPh60BCMUzdk2gYUYySv9UVSGAhqH4dQPWZ0l9USmYA==", + "resolved": "0.10.11.5", + "contentHash": "XHU7/hRYWEdaJOs+weT9ml9/GYqroPrAtePjGzUzUrMoHESbqmkLXmW+fHkaAeXRMJAOAFD1LQUHQu+B6ThF3w==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "OAwrl0tzsz+iX2z3T5wdsEG/JkbnlylfXUjCfbgQlQqODUBO0kk02RpCHJMnV86apkVXjeLV+S5+yA1yX+DT6g==", + "resolved": "0.10.11.5", + "contentHash": "MpqS1t1zypDNEW2Pyg113W4AwmfaWai5LfA/K22sDbygXII+KuACaHt2ZPHJWnvKGHgasLEEFhEOGfF5cB9NPA==", "dependencies": { - "Avalonia": "0.10.10", - "Avalonia.Xaml.Interactivity": "0.10.10.4" + "Avalonia": "0.10.11", + "Avalonia.Xaml.Interactivity": "0.10.11.5" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.10.4", - "contentHash": "RH33/HboQikon64SCfu1qFpuy4+mox2SEGYbXrrw9mnw3Ogw8PeYTJLtZw8/mNYs6dZAU3NusRBaLIG+0PGisw==", + "resolved": "0.10.11.5", + "contentHash": "MOM6lcPenJZu9LPhai3n67GssBUx3MjoCVLyR2GEJ6lywKQgk4MKOIAC0gLWgy7x1e540oy4lCpeX8jMsqe7mA==", "dependencies": { - "Avalonia": "0.10.10" + "Avalonia": "0.10.11" } }, "Castle.Core": { @@ -277,20 +294,37 @@ }, "HarfBuzzSharp": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "/ZDcKBMxStEjQs9MpSP3abSBJsdoWzkCQFZ8zRpDFAvblDysHazEhUTRyUYD/fVczlH6jX5V1EYCiITtdPz00w==", + "resolved": "2.8.2-preview.178", + "contentHash": "OUir5qn95QRtlc8RWKfU/63xYwtuAbylL2oAj3eBWgAsVoWnFrEv+Oh1sj0xjW7mogFGaeGtY40lqAD1srWJcQ==", "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2-preview.178", "System.Memory": "4.5.3" } }, "HarfBuzzSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.6.1.7", - "contentHash": "Aef8YgAqJ5dW0CNWCtaPNKHdJlhl+w83LmqDpaan9NmmKhG/px6Zta06iu0R2n4RrBHCRDly/Q/6kc0xQOfT9A==", + "resolved": "2.8.2-preview.178", + "contentHash": "4scihdELcRpCEubBsUMHUJn93Xvx6ASj596WfO9y8CEuFNW0LBMDL71HBCyq5zXsn8HyGjLtoBLW0PpXbVnpjQ==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7" + "HarfBuzzSharp": "2.8.2-preview.178" } }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "QtmAs62il4vFtt3fFOXhhPDl7TX+NGu4tFB5qmnqUn+EnSJW7mxqNk1n9I7+Z2ORym0nTP4dhcRNtOpOS7Oenw==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "+S8qtBAVTrt+E85jZXPxYthUgSUq7iB6UZ0v0WFsy9gWhZ/hVE3hZJpcgeywT9H/SRX3ZIX+qzpKJlOM+mUcNA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "EgeF5uCZcAriIHmWq3hKNxz/jBJLeP/PKU4yI87UNkJCt4hYignOMjY0irl/rGVZtTL/G05xxf7TB6sjisi8sQ==" + }, "HidSharp": { "type": "Transitive", "resolved": "2.1.0", @@ -537,6 +571,14 @@ "Splat": "13.1.1" } }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -675,43 +717,60 @@ }, "ShimSkiaSharp": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "G1ltdwS5+eMhMCoMx31hHjFIznGAjdUK7xAg8raFMFbN09p2tuxzvK7cKbveWm8SEAGIW7NgDyEqGGJzrPrMrg==" + "resolved": "0.5.11", + "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, "SkiaSharp.HarfBuzz": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "CakjlLUm22f4qy0WEyGVAqNuewgJre1qyUqEpg2Vi6jwEnTE42aCG607CL+i9LvSjdOdKOh/Dm6hESuLPYoTbw==", + "resolved": "2.88.0-preview.178", + "contentHash": "qTf3PbRJsTVOttQkYWOCswa1QnkH0dEOqMSgr3iXZQuKPNlj1qpGY8+OGPs25WKgUEqOpv2nog/AYQ/bpyOXzA==", "dependencies": { - "HarfBuzzSharp": "2.6.1.7", - "SkiaSharp": "2.80.2" + "HarfBuzzSharp": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.80.2", - "contentHash": "uQSxFy5iVTK6tENWrlc+HCKGSCLgJ+d2KGXUlC1OMCXlKOVkzMqdwa0gMukrEA6HYdO+qk6IUq3ya4fk70EB4g==", + "resolved": "2.88.0-preview.178", + "contentHash": "nc9C8zGvL2G7p0lcTPhN4EOt2Mozv6KLJinMwjF97sYoI5cpkXCPZSRTcyf8k49gAZaOd+UMGaygCAz/8vaaWg==", "dependencies": { - "SkiaSharp": "2.80.2" + "SkiaSharp": "2.88.0-preview.178" } }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "u4Ss81oOlx0dhu5fxl4vK5f2Hm7psHDUSAoQValNV/BmixsW4TkETE3dOnHNRWwI56++tRG9dK33HimZDUrUpw==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "Splat": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" + "resolved": "14.1.17", + "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" }, "Svg.Custom": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Ypi/4NxDxjM24vsK+4Uit08aJUFHcZZpB5Ctb7FLMVevMq0hYeDqzKp6OVLFO5UCX/TxUfRiL1u9F7fvUDA0tQ==", + "resolved": "0.5.11", + "contentHash": "8OEW3UKx07JfEyqzzvF5+ycydusZjg6jsBjDSBrAoq62c5gNZrs6brlOKm2ywEj9hObK3sLcat5BHnE2OUHXsg==", "dependencies": { "Fizzler": "1.2.0", "System.Drawing.Common": "5.0.0", @@ -722,22 +781,22 @@ }, "Svg.Model": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "sh5W7VpghtFjDnPOMa+CEiIKpJPmkb7FxNuHnB2sLZwJR2qzyVlZaBWM95VaRAXlOU8c0qbtA5ZNVREOpzLQRw==", + "resolved": "0.5.11", + "contentHash": "5/Y+BGjgTwobA9aDfcpTGF/bm83MYrEYM8J1LpPohRR+c+B/1N+rbSXfpDZq2omBJ1O0Sa5VjAXw1oAdm1lYLg==", "dependencies": { - "ShimSkiaSharp": "0.5.10", - "Svg.Custom": "0.5.10" + "ShimSkiaSharp": "0.5.11", + "Svg.Custom": "0.5.11" } }, "Svg.Skia": { "type": "Transitive", - "resolved": "0.5.10", - "contentHash": "Xz/nd+5dNJAh7IbfjyduWIJAtpHNqMopX+ORg6ZNnW6l1I7lK20KHlSdMNy9c18vALrA95eU8WFo00nloLVUkQ==", + "resolved": "0.5.11", + "contentHash": "AiN5rSsYBzBUkoh8YK5HoNxRxHtUekp2/6ZAol8qV8oDr6vu8hmPuWjUDwvqCFMi9Dlllc6YsFfvJ1PZCJvYew==", "dependencies": { - "SkiaSharp": "2.80.2", - "SkiaSharp.HarfBuzz": "2.80.2", - "Svg.Custom": "0.5.10", - "Svg.Model": "0.5.10" + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.HarfBuzz": "2.88.0-preview.178", + "Svg.Custom": "0.5.11", + "Svg.Model": "0.5.11" } }, "System.AppContext": { @@ -1669,6 +1728,9 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", @@ -1692,14 +1754,15 @@ "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.10", - "Avalonia.ReactiveUI": "0.10.10", - "Avalonia.Svg.Skia": "0.10.10", - "Avalonia.Xaml.Behaviors": "0.10.10.4", - "Avalonia.Xaml.Interactions": "0.10.10.4", - "Avalonia.Xaml.Interactivity": "0.10.10.4", - "FluentAvaloniaUI": "1.1.7", + "Avalonia": "0.10.11", + "Avalonia.ReactiveUI": "0.10.11", + "Avalonia.Svg.Skia": "0.10.11", + "Avalonia.Xaml.Behaviors": "0.10.11.5", + "Avalonia.Xaml.Interactions": "0.10.11.5", + "Avalonia.Xaml.Interactivity": "0.10.11.5", + "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease1", "ReactiveUI.Validation": "2.2.1" } } From 34757716aa8d38e99f0b37c5f4f4471cc89c4020 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 30 Dec 2021 23:33:26 +0100 Subject: [PATCH 099/270] Updated ReactiveUI Added order to render element ctors --- src/Artemis.Core/Models/Profile/Folder.cs | 5 +- src/Artemis.Core/Models/Profile/Layer.cs | 5 +- src/Artemis.Core/Models/Profile/Profile.cs | 2 +- src/Artemis.Core/Utilities/Utilities.cs | 3 + .../ProfileTree/TreeItem/TreeItemViewModel.cs | 4 +- .../Artemis.UI.Linux/Artemis.UI.Linux.csproj | 1 + .../Artemis.UI.Linux/packages.lock.json | 46 +++- .../Artemis.UI.MacOS/Artemis.UI.MacOS.csproj | 1 + .../Artemis.UI.MacOS/packages.lock.json | 46 +++- .../Artemis.UI.Shared.csproj | 1 + .../Services/Builders/NotificationBuilder.cs | 2 +- .../Artemis.UI.Shared/Styles/Border.axaml | 4 + .../Artemis.UI.Shared/Styles/Button.axaml | 18 +- .../Artemis.UI.Shared/packages.lock.json | 48 ++-- .../ApplicationStateManager.cs | 8 +- .../Artemis.UI.Windows.csproj | 1 + .../Artemis.UI.Windows/packages.lock.json | 46 +++- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 6 + src/Avalonia/Artemis.UI/MainWindow.axaml | 30 ++- src/Avalonia/Artemis.UI/MainWindow.axaml.cs | 12 +- .../Ninject/Factories/IVMFactory.cs | 4 +- src/Avalonia/Artemis.UI/ReactiveCoreWindow.cs | 65 ++---- .../Artemis.UI/Screens/MainScreenViewModel.cs | 2 + .../Screens/Plugins/PluginFeatureViewModel.cs | 6 +- .../Panels/MenuBar/MenuBarView.axaml | 198 +++++++++++++++++ .../Panels/MenuBar/MenuBarView.axaml.cs | 25 +++ .../Panels/MenuBar/MenuBarViewModel.cs | 13 ++ .../ProfileTree/FolderTreeItemView.axaml | 3 +- .../ProfileTree/FolderTreeItemViewModel.cs | 14 +- .../ProfileTree/LayerTreeItemView.axaml | 7 +- .../ProfileTree/LayerTreeItemViewModel.cs | 8 +- .../Panels/ProfileTree/ProfileTreeView.axaml | 32 ++- .../ProfileTree/ProfileTreeViewModel.cs | 78 ++++++- .../Panels/ProfileTree/TreeItemViewModel.cs | 49 ++++- .../ProfileEditorTitleBarView.axaml | 18 ++ .../ProfileEditorTitleBarView.axaml.cs | 25 +++ .../ProfileEditorTitleBarViewModel.cs | 24 ++ .../ProfileEditor/ProfileEditorView.axaml | 208 +----------------- .../ProfileEditor/ProfileEditorViewModel.cs | 17 +- .../Screens/Root/DefaultTitleBarView.axaml | 11 + .../Screens/Root/DefaultTitleBarView.axaml.cs | 19 ++ .../Screens/Root/DefaultTitleBarViewModel.cs | 20 ++ .../Artemis.UI/Screens/Root/RootView.axaml | 2 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 23 +- .../Services/ProfileEditorService.cs | 3 +- src/Avalonia/Artemis.UI/packages.lock.json | 45 +++- 46 files changed, 838 insertions(+), 370 deletions(-) create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 7ef74f1d1..6a02aa2ec 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -22,7 +22,8 @@ namespace Artemis.Core /// /// The parent of the folder /// The name of the folder - public Folder(ProfileElement parent, string name) : base(parent.Profile) + /// The order where to place the child (0-based), defaults to the end of the collection + public Folder(ProfileElement parent, string name, int order) : base(parent.Profile) { FolderEntity = new FolderEntity(); EntityId = Guid.NewGuid(); @@ -31,7 +32,7 @@ namespace Artemis.Core Profile = Parent.Profile; Name = name; - Parent.AddChild(this); + Parent.AddChild(this, order); } /// diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index dfb0a68dc..b1ccccfc6 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -29,7 +29,8 @@ namespace Artemis.Core /// /// The parent of the layer /// The name of the layer - public Layer(ProfileElement parent, string name) : base(parent.Profile) + /// The order where to place the child (0-based), defaults to the end of the collection + public Layer(ProfileElement parent, string name, int order) : base(parent.Profile) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); @@ -47,7 +48,7 @@ namespace Artemis.Core Adapter = new LayerAdapter(this); Initialize(); - Parent.AddChild(this, 0); + Parent.AddChild(this, order); } /// diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index a9090e12d..a905c3017 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -198,7 +198,7 @@ namespace Artemis.Core FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId); if (rootFolder == null) { - Folder _ = new(this, "Root folder"); + Folder _ = new(this, "Root folder", 0); } else { diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index e6391399a..42774d5d9 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -44,6 +44,9 @@ namespace Artemis.Core /// A list of extra arguments to pass to Artemis when restarting public static void Restart(bool elevate, TimeSpan delay, params string[] extraArgs) { + if (!OperatingSystem.IsWindows() && elevate) + throw new ArtemisCoreException("Elevation on non-Windows platforms is not supported."); + OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index 58d6560fd..d467c25a2 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -137,7 +137,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!SupportsChildren) throw new ArtemisUIException("Cannot add a folder to a profile element of type " + ProfileElement.GetType().Name); - Folder _ = new(ProfileElement, "New folder"); + Folder _ = new(ProfileElement, "New folder", 0); _profileEditorService.SaveSelectedProfileConfiguration(); } @@ -146,7 +146,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!SupportsChildren) throw new ArtemisUIException("Cannot add a layer to a profile element of type " + ProfileElement.GetType().Name); - Layer layer = new(ProfileElement, "New layer"); + Layer layer = new(ProfileElement, "New layer", 0); // Could be null if the default brush got disabled LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); diff --git a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 69e1b2744..95b799e45 100644 --- a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index bb01b4430..251ccdd08 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -53,6 +53,20 @@ "System.Reactive": "5.0.0" } }, + "ReactiveUI": { + "type": "Direct", + "requested": "[17.1.9, )", + "resolved": "17.1.9", + "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "dependencies": { + "DynamicData": "7.4.3", + "Splat": "14.1.1", + "System.ComponentModel": "4.3.0", + "System.Diagnostics.Contracts": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", "resolved": "2.1.0.2020091801", @@ -191,8 +205,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "E9oTvWlAgzct0MuWt6k+0s+nSDA3LkFVvDwkMUTklIZZnva314KZAEF2vG4XX9I98ia+EpMqjte67jWEBJlsRw==", + "resolved": "7.4.3", + "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", "dependencies": { "System.Reactive": "5.0.0" } @@ -522,15 +536,6 @@ "Ninject": "3.3.3" } }, - "ReactiveUI": { - "type": "Transitive", - "resolved": "16.2.6", - "contentHash": "jf1RvD8HxHuA6CGQtheGHUCHzRrhpvo0z593Npsz7g8KJWXfGR45Dc9bILJHoymBxhdDD1L1WjUfh0fcucIPPg==", - "dependencies": { - "DynamicData": "7.3.1", - "Splat": "13.1.1" - } - }, "ReactiveUI.Validation": { "type": "Transitive", "resolved": "2.2.1", @@ -918,6 +923,14 @@ "System.Text.Encoding": "4.3.0" } }, + "System.Diagnostics.Contracts": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1398,6 +1411,15 @@ "System.Runtime.Extensions": "4.3.0" } }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1757,6 +1779,7 @@ "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", "RGB.NET.Layout": "1.0.0-prerelease1", + "ReactiveUI": "17.1.9", "ReactiveUI.Validation": "2.2.1", "Splat.Ninject": "14.1.17" } @@ -1774,6 +1797,7 @@ "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", + "ReactiveUI": "17.1.9", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index 69e1b2744..95b799e45 100644 --- a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index bb01b4430..251ccdd08 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -53,6 +53,20 @@ "System.Reactive": "5.0.0" } }, + "ReactiveUI": { + "type": "Direct", + "requested": "[17.1.9, )", + "resolved": "17.1.9", + "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "dependencies": { + "DynamicData": "7.4.3", + "Splat": "14.1.1", + "System.ComponentModel": "4.3.0", + "System.Diagnostics.Contracts": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", "resolved": "2.1.0.2020091801", @@ -191,8 +205,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "E9oTvWlAgzct0MuWt6k+0s+nSDA3LkFVvDwkMUTklIZZnva314KZAEF2vG4XX9I98ia+EpMqjte67jWEBJlsRw==", + "resolved": "7.4.3", + "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", "dependencies": { "System.Reactive": "5.0.0" } @@ -522,15 +536,6 @@ "Ninject": "3.3.3" } }, - "ReactiveUI": { - "type": "Transitive", - "resolved": "16.2.6", - "contentHash": "jf1RvD8HxHuA6CGQtheGHUCHzRrhpvo0z593Npsz7g8KJWXfGR45Dc9bILJHoymBxhdDD1L1WjUfh0fcucIPPg==", - "dependencies": { - "DynamicData": "7.3.1", - "Splat": "13.1.1" - } - }, "ReactiveUI.Validation": { "type": "Transitive", "resolved": "2.2.1", @@ -918,6 +923,14 @@ "System.Text.Encoding": "4.3.0" } }, + "System.Diagnostics.Contracts": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1398,6 +1411,15 @@ "System.Runtime.Extensions": "4.3.0" } }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1757,6 +1779,7 @@ "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", "RGB.NET.Layout": "1.0.0-prerelease1", + "ReactiveUI": "17.1.9", "ReactiveUI.Validation": "2.2.1", "Splat.Ninject": "14.1.17" } @@ -1774,6 +1797,7 @@ "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", + "ReactiveUI": "17.1.9", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 8d084dfdd..1ceca6a48 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs index 266f71062..e66117255 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs @@ -123,7 +123,7 @@ namespace Artemis.UI.Shared.Services.Builders public IControl Build() { return _action != null - ? new Button {Content = _text, Command = ReactiveCommand.Create(() => _action)} + ? new Button {Content = _text, Command = ReactiveCommand.Create(() => _action())} : new Button {Content = _text}; } } diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml index b1d279fb3..27e3bf784 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml @@ -25,6 +25,10 @@ + + + + + + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 1d1ddb99e..1e2b76874 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using System.Reactive.Disposables; using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Services; using Artemis.UI.Shared; +using HistoricalReactiveCommand; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree @@ -13,15 +16,48 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public class ProfileTreeViewModel : ActivatableViewModelBase { private readonly IProfileEditorVmFactory _profileEditorVmFactory; + private TreeItemViewModel? _selectedTreeItem; public ProfileTreeViewModel(IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) { _profileEditorVmFactory = profileEditorVmFactory; - this.WhenActivated(d => profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(Repopulate).DisposeWith(d)); + + this.WhenActivated(d => + { + ProfileConfiguration profileConfiguration = null!; + profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => profileConfiguration = p).DisposeWith(d); + profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(Repopulate).DisposeWith(d); + profileEditorService.CurrentProfileElement.WhereNotNull().Subscribe(SelectCurrentProfileElement).DisposeWith(d); + + Folder rootFolder = profileConfiguration.Profile!.GetRootFolder(); + AddLayer = ReactiveCommandEx.CreateWithHistory("AddLayerAtRoot", + // ReSharper disable once ObjectCreationAsStatement + () => new Layer(rootFolder, "New layer", 0), + () => rootFolder.RemoveChild(rootFolder.Children[0]), + profileConfiguration.Profile.EntityId.ToString() + ); + + AddFolder = ReactiveCommandEx.CreateWithHistory("AddFolderAtRoot", + // ReSharper disable once ObjectCreationAsStatement + () => new Folder(rootFolder, "New folder", 0), + () => rootFolder.RemoveChild(rootFolder.Children[0]), + profileConfiguration.Profile.EntityId.ToString() + ); + }); + this.WhenAnyValue(vm => vm.SelectedTreeItem).Subscribe(model => profileEditorService.ChangeCurrentProfileElement(model?.ProfileElement)); } + public ReactiveCommandWithHistory? AddLayer { get; set; } + public ReactiveCommandWithHistory? AddFolder { get; set; } + public ObservableCollection TreeItems { get; } = new(); + public TreeItemViewModel? SelectedTreeItem + { + get => _selectedTreeItem; + set => this.RaiseAndSetIfChanged(ref _selectedTreeItem, value); + } + private void Repopulate(ProfileConfiguration profileConfiguration) { if (TreeItems.Any()) @@ -33,11 +69,43 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree foreach (ProfileElement profileElement in profileConfiguration.Profile.GetRootFolder().Children) { if (profileElement is Folder folder) - TreeItems.Add(_profileEditorVmFactory.FolderTreeItemViewModel(folder)); + TreeItems.Add(_profileEditorVmFactory.FolderTreeItemViewModel(null, folder)); else if (profileElement is Layer layer) - TreeItems.Add(_profileEditorVmFactory.LayerTreeItemViewModel(layer)); + TreeItems.Add(_profileEditorVmFactory.LayerTreeItemViewModel(null, layer)); } - + } + + private void SelectCurrentProfileElement(RenderProfileElement element) + { + if (SelectedTreeItem?.ProfileElement == element) + return; + + // Find the tree item belonging to the selected element + List treeItems = GetAllTreeItems(TreeItems); + TreeItemViewModel? selected = treeItems.FirstOrDefault(e => e.ProfileElement == element); + + // Walk up the tree, expanding parents + TreeItemViewModel? currentParent = selected?.Parent; + while (currentParent != null) + { + currentParent.IsExpanded = true; + currentParent = currentParent.Parent; + } + + SelectedTreeItem = selected; + } + + private List GetAllTreeItems(ObservableCollection treeItems) + { + List result = new(); + foreach (TreeItemViewModel treeItemViewModel in treeItems) + { + result.Add(treeItemViewModel); + if (treeItemViewModel.Children.Any()) + result.AddRange(GetAllTreeItems(treeItemViewModel.Children)); + } + + return result; } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 28397234e..2497df852 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -1,8 +1,55 @@ -using Artemis.UI.Shared; +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using HistoricalReactiveCommand; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public abstract class TreeItemViewModel : ActivatableViewModelBase { + private bool _isExpanded; + + protected TreeItemViewModel(TreeItemViewModel? parent, RenderProfileElement profileElement) + { + Parent = parent; + ProfileElement = profileElement; + + AddLayerAtIndex = ReactiveCommandEx.CreateWithHistory("AddLayerAtIndex", + (targetIndex, _) => new Layer(ProfileElement, "New folder", targetIndex), + (targetIndex, _) => + { + Layer toRemove = (Layer) ProfileElement.Children.ElementAt(targetIndex); + ProfileElement.RemoveChild(toRemove); + return toRemove; + }, + historyId: ProfileElement.Profile.EntityId.ToString() + ); + + AddFolderAtIndex = ReactiveCommandEx.CreateWithHistory("AddFolderAtIndex", + (targetIndex, _) => new Folder(ProfileElement, "New folder", targetIndex), + (targetIndex, _) => + { + Folder toRemove = (Folder) ProfileElement.Children.ElementAt(targetIndex); + ProfileElement.RemoveChild(toRemove); + return toRemove; + }, + historyId: ProfileElement.Profile.EntityId.ToString() + ); + } + + public ReactiveCommandWithHistory AddLayerAtIndex { get; set; } + public ReactiveCommandWithHistory AddFolderAtIndex { get; set; } + + public RenderProfileElement ProfileElement { get; } + public TreeItemViewModel? Parent { get; set; } + public ObservableCollection Children { get; } = new(); + + public bool IsExpanded + { + get => _isExpanded; + set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml new file mode 100644 index 000000000..62afce2c8 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs new file mode 100644 index 000000000..08b7a31c3 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.ProfileEditor +{ + public partial class ProfileEditorTitleBarView : UserControl + { + public ProfileEditorTitleBarView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void MenuItem_OnSubmenuOpened(object? sender, RoutedEventArgs e) + { + + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs new file mode 100644 index 000000000..c30c92144 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarViewModel.cs @@ -0,0 +1,24 @@ +using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.ProfileEditor +{ + public class ProfileEditorTitleBarViewModel : ViewModelBase + { + private readonly IDebugService _debugService; + + public ProfileEditorTitleBarViewModel(IDebugService debugService, MenuBarViewModel menuBarViewModel) + { + MenuBarViewModel = menuBarViewModel; + _debugService = debugService; + } + + public MenuBarViewModel MenuBarViewModel { get; } + + public void ShowDebugger() + { + _debugService.ShowDebugger(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index a64869c3d..1d04329a8 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -24,204 +24,19 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - @@ -239,7 +54,6 @@ - @@ -249,9 +63,9 @@ - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index da3b0978b..63b13aa40 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar; using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Services; @@ -13,16 +14,28 @@ namespace Artemis.UI.Screens.ProfileEditor private ProfileConfiguration? _profile; /// - public ProfileEditorViewModel(IScreen hostScreen, IProfileEditorService profileEditorService, VisualEditorViewModel visualEditorViewModel, ProfileTreeViewModel profileTreeViewModel) + public ProfileEditorViewModel(IScreen hostScreen, + IProfileEditorService profileEditorService, + VisualEditorViewModel visualEditorViewModel, + ProfileTreeViewModel profileTreeViewModel, + ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, + MenuBarViewModel menuBarViewModel) : base(hostScreen, "profile-editor") { VisualEditorViewModel = visualEditorViewModel; ProfileTreeViewModel = profileTreeViewModel; - this.WhenActivated(disposables => { profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => Profile = p).DisposeWith(disposables); }); + + if (OperatingSystem.IsWindows()) + TitleBarViewModel = profileEditorTitleBarViewModel; + else + MenuBarViewModel = menuBarViewModel; + + this.WhenActivated(disposables => profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => Profile = p).DisposeWith(disposables)); } public VisualEditorViewModel VisualEditorViewModel { get; } public ProfileTreeViewModel ProfileTreeViewModel { get; } + public MenuBarViewModel? MenuBarViewModel { get; } public ProfileConfiguration? Profile { diff --git a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml new file mode 100644 index 000000000..3ff09a332 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml.cs new file mode 100644 index 000000000..24d3e8070 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Root +{ + public partial class DefaultTitleBarView : UserControl + { + public DefaultTitleBarView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarViewModel.cs new file mode 100644 index 000000000..1f5060c7c --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarViewModel.cs @@ -0,0 +1,20 @@ +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.Root +{ + public class DefaultTitleBarViewModel : ViewModelBase + { + private readonly IDebugService _debugService; + + public DefaultTitleBarViewModel(IDebugService debugService) + { + _debugService = debugService; + } + + public void ShowDebugger() + { + _debugService.ShowDebugger(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml index 6c5dde4ca..991a035dd 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml @@ -11,7 +11,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 1301bb454..52f22ac97 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -1,4 +1,5 @@ -using System; + +using System; using System.Linq; using System.Threading.Tasks; using Artemis.Core; @@ -21,6 +22,7 @@ namespace Artemis.UI.Screens.Root public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvider { private readonly IAssetLoader _assetLoader; + private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel; private readonly ICoreService _coreService; private readonly IDebugService _debugService; private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; @@ -30,6 +32,7 @@ namespace Artemis.UI.Screens.Root private SidebarViewModel? _sidebarViewModel; private TrayIcon? _trayIcon; private TrayIcons? _trayIcons; + private ViewModelBase? _titleBarViewModel; public RootViewModel(ICoreService coreService, ISettingsService settingsService, @@ -38,6 +41,7 @@ namespace Artemis.UI.Screens.Root IMainWindowService mainWindowService, IDebugService debugService, IAssetLoader assetLoader, + DefaultTitleBarViewModel defaultTitleBarViewModel, ISidebarVmFactory sidebarVmFactory) { Router = new RoutingState(); @@ -47,6 +51,7 @@ namespace Artemis.UI.Screens.Root _windowService = windowService; _debugService = debugService; _assetLoader = assetLoader; + _defaultTitleBarViewModel = defaultTitleBarViewModel; _sidebarVmFactory = sidebarVmFactory; _lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current.ApplicationLifetime; @@ -54,15 +59,31 @@ namespace Artemis.UI.Screens.Root mainWindowService.ConfigureMainWindowProvider(this); DisplayAccordingToSettings(); + Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel); Task.Run(coreService.Initialize); } + private void UpdateTitleBarViewModel(IRoutableViewModel? viewModel) + { + if (viewModel is MainScreenViewModel mainScreenViewModel && mainScreenViewModel.TitleBarViewModel != null) + TitleBarViewModel = mainScreenViewModel.TitleBarViewModel; + else + TitleBarViewModel = _defaultTitleBarViewModel; + } + + public SidebarViewModel? SidebarViewModel { get => _sidebarViewModel; set => this.RaiseAndSetIfChanged(ref _sidebarViewModel, value); } + public ViewModelBase? TitleBarViewModel + { + get => _titleBarViewModel; + set => this.RaiseAndSetIfChanged(ref _titleBarViewModel, value); + } + private void CurrentMainWindowOnClosed(object? sender, EventArgs e) { _lifeTime.MainWindow = null; diff --git a/src/Avalonia/Artemis.UI/Services/ProfileEditorService.cs b/src/Avalonia/Artemis.UI/Services/ProfileEditorService.cs index 966bbc8ac..51b299614 100644 --- a/src/Avalonia/Artemis.UI/Services/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI/Services/ProfileEditorService.cs @@ -22,8 +22,7 @@ namespace Artemis.UI.Services CurrentProfileConfiguration = Observable.Defer(() => Observable.Return(_currentProfileConfiguration)).Concat(_changeCurrentProfileConfiguration); CurrentProfileElement = Observable.Defer(() => Observable.Return(_currentProfileElement)).Concat(_changeCurrentProfileElement); } - - + public IObservable CurrentProfileConfiguration { get; } public IObservable CurrentProfileElement { get; } diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index 908975a90..36aa30742 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -115,6 +115,20 @@ "Material.Icons": "1.0.2" } }, + "ReactiveUI": { + "type": "Direct", + "requested": "[17.1.9, )", + "resolved": "17.1.9", + "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "dependencies": { + "DynamicData": "7.4.3", + "Splat": "14.1.1", + "System.ComponentModel": "4.3.0", + "System.Diagnostics.Contracts": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, "ReactiveUI.Validation": { "type": "Direct", "requested": "[2.2.1, )", @@ -268,8 +282,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.3.1", - "contentHash": "E9oTvWlAgzct0MuWt6k+0s+nSDA3LkFVvDwkMUTklIZZnva314KZAEF2vG4XX9I98ia+EpMqjte67jWEBJlsRw==", + "resolved": "7.4.3", + "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", "dependencies": { "System.Reactive": "5.0.0" } @@ -562,15 +576,6 @@ "Ninject": "3.3.3" } }, - "ReactiveUI": { - "type": "Transitive", - "resolved": "16.2.6", - "contentHash": "jf1RvD8HxHuA6CGQtheGHUCHzRrhpvo0z593Npsz7g8KJWXfGR45Dc9bILJHoymBxhdDD1L1WjUfh0fcucIPPg==", - "dependencies": { - "DynamicData": "7.3.1", - "Splat": "13.1.1" - } - }, "RGB.NET.Presets": { "type": "Transitive", "resolved": "1.0.0-prerelease1", @@ -928,6 +933,14 @@ "System.Text.Encoding": "4.3.0" } }, + "System.Diagnostics.Contracts": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1408,6 +1421,15 @@ "System.Runtime.Extensions": "4.3.0" } }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1763,6 +1785,7 @@ "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", + "ReactiveUI": "17.1.9", "ReactiveUI.Validation": "2.2.1" } } From 736324e45ee62c9b8235c5a202bb413c7f80fb81 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 2 Jan 2022 00:04:46 +0100 Subject: [PATCH 100/270] Profile editor - Added unro/redo Profile editor - Added adding layers and folders --- src/Artemis.ConsoleUI/packages.lock.json | 1271 +++++++++++++++++ .../Profiles/ProfileElementEventArgs.cs | 20 + src/Artemis.Core/Models/Profile/Folder.cs | 9 +- src/Artemis.Core/Models/Profile/Layer.cs | 50 +- src/Artemis.Core/Models/Profile/Profile.cs | 6 +- .../Models/Profile/ProfileElement.cs | 16 +- .../Models/Profile/RenderProfileElement.cs | 17 +- .../ProfileTree/TreeItem/TreeItemViewModel.cs | 7 +- .../Tools/SelectionToolViewModel.cs | 1 + .../Artemis.UI.Linux/Artemis.UI.Linux.csproj | 2 +- .../Artemis.UI.Linux/packages.lock.json | 33 +- .../Artemis.UI.MacOS/Artemis.UI.MacOS.csproj | 2 +- .../Artemis.UI.MacOS/packages.lock.json | 33 +- .../Artemis.UI.Shared.csproj | 2 +- .../Artemis.UI.Shared/packages.lock.json | 33 +- .../Artemis.UI.Windows.csproj | 2 +- .../Artemis.UI.Windows/packages.lock.json | 33 +- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 7 +- .../Commands/AddProfileElement.cs | 56 + .../Commands/RemoveProfileElement.cs | 63 + .../Panels/MenuBar/MenuBarView.axaml | 23 +- .../Panels/MenuBar/MenuBarView.axaml.cs | 3 +- .../Panels/MenuBar/MenuBarViewModel.cs | 23 +- .../ProfileTree/FolderTreeItemView.axaml | 6 +- .../ProfileTree/FolderTreeItemViewModel.cs | 3 +- .../ProfileTree/LayerTreeItemView.axaml | 4 +- .../ProfileTree/LayerTreeItemViewModel.cs | 3 +- .../ProfileTree/ProfileTreeViewModel.cs | 88 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 70 +- .../VisualEditor/VisualEditorViewModel.cs | 1 + .../ProfileEditor/ProfileEditorView.axaml | 4 + .../ProfileEditor/ProfileEditorViewModel.cs | 23 +- .../Sidebar/SidebarCategoryViewModel.cs | 3 +- .../Screens/Sidebar/SidebarViewModel.cs | 3 +- .../ProfileEditor/IProfileEditorCommand.cs | 23 + .../ProfileEditor/IProfileEditorService.cs | 17 + .../ProfileEditor/ProfileEditorHistory.cs | 96 ++ .../ProfileEditor/ProfileEditorService.cs | 58 + .../Services/ProfileEditorService.cs | 50 - src/Avalonia/Artemis.UI/packages.lock.json | 31 +- 40 files changed, 1878 insertions(+), 317 deletions(-) create mode 100644 src/Artemis.ConsoleUI/packages.lock.json create mode 100644 src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs create mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorCommand.cs create mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditor/IProfileEditorService.cs create mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorHistory.cs create mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditor/ProfileEditorService.cs delete mode 100644 src/Avalonia/Artemis.UI/Services/ProfileEditorService.cs diff --git a/src/Artemis.ConsoleUI/packages.lock.json b/src/Artemis.ConsoleUI/packages.lock.json new file mode 100644 index 000000000..e09690669 --- /dev/null +++ b/src/Artemis.ConsoleUI/packages.lock.json @@ -0,0 +1,1271 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Humanizer.Core": { + "type": "Direct", + "requested": "[2.11.10, )", + "resolved": "2.11.10", + "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" + }, + "Ninject": { + "type": "Direct", + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", + "dependencies": { + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0" + } + }, + "Ninject.Extensions.Conventions": { + "type": "Direct", + "requested": "[3.3.0, )", + "resolved": "3.3.0", + "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", + "dependencies": { + "Ninject.Extensions.Factory": "3.3.2" + } + }, + "Serilog": { + "type": "Direct", + "requested": "[2.10.0, )", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "System.Buffers": { + "type": "Direct", + "requested": "[4.5.1, )", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.ComponentModel.Annotations": { + "type": "Direct", + "requested": "[5.0.0, )", + "resolved": "5.0.0", + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" + }, + "System.Numerics.Vectors": { + "type": "Direct", + "requested": "[4.5.0, )", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ValueTuple": { + "type": "Direct", + "requested": "[4.5.0, )", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "HidSharp": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" + }, + "LiteDB": { + "type": "Transitive", + "resolved": "5.0.11", + "contentHash": "6cL4bOmVCUB0gIK+6qIr68HeqjjHZicPDGQjvJ87mIOvkFsEsJWkIps3yoKNeLpHhJQur++yoQ9Q8gxsdos0xQ==" + }, + "McMaster.NETCore.Plugins": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Ninject.Extensions.ChildKernel": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", + "dependencies": { + "Ninject": "3.3.4" + } + }, + "Ninject.Extensions.Factory": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", + "dependencies": { + "Castle.Core": "4.2.0", + "Ninject": "3.3.3" + } + }, + "RGB.NET.Core": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease1", + "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease1" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "SkiaSharp": { + "type": "Transitive", + "resolved": "2.80.3", + "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, + "artemis.core": { + "type": "Project", + "dependencies": { + "Artemis.Storage": "1.0.0", + "EmbedIO": "3.4.3", + "HidSharp": "2.1.0", + "Humanizer.Core": "2.11.10", + "LiteDB": "5.0.11", + "McMaster.NETCore.Plugins": "1.4.0", + "Newtonsoft.Json": "13.0.1", + "Ninject": "3.3.4", + "Ninject.Extensions.ChildKernel": "3.3.0", + "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Presets": "1.0.0-prerelease1", + "Serilog": "2.10.0", + "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.0.0", + "SkiaSharp": "2.80.3", + "System.Buffers": "4.5.1", + "System.IO.FileSystem.AccessControl": "5.0.0", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "5.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "artemis.storage": { + "type": "Project", + "dependencies": { + "LiteDB": "5.0.11", + "Serilog": "2.10.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs b/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs new file mode 100644 index 000000000..4f4a740e4 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data for profile element events. + /// + public class ProfileElementEventArgs : EventArgs + { + internal ProfileElementEventArgs(ProfileElement profileElement) + { + ProfileElement = profileElement; + } + + /// + /// Gets the profile element this event is related to + /// + public ProfileElement ProfileElement { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 6a02aa2ec..47e0694c6 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -22,17 +22,13 @@ namespace Artemis.Core /// /// The parent of the folder /// The name of the folder - /// The order where to place the child (0-based), defaults to the end of the collection - public Folder(ProfileElement parent, string name, int order) : base(parent.Profile) + public Folder(ProfileElement parent, string name) : base(parent, parent.Profile) { FolderEntity = new FolderEntity(); EntityId = Guid.NewGuid(); - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Profile = Parent.Profile; Name = name; - - Parent.AddChild(this, order); } /// @@ -41,13 +37,12 @@ namespace Artemis.Core /// The profile the folder belongs to /// The parent of the folder /// The entity of the folder - public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent.Profile) + public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile) { FolderEntity = folderEntity; EntityId = folderEntity.Id; Profile = profile; - Parent = parent; Name = folderEntity.Name; IsExpanded = folderEntity.IsExpanded; Suspended = folderEntity.Suspended; diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index b1ccccfc6..90bf0b309 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -4,7 +4,6 @@ using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; -using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; using RGB.NET.Core; @@ -29,13 +28,11 @@ namespace Artemis.Core /// /// The parent of the layer /// The name of the layer - /// The order where to place the child (0-based), defaults to the end of the collection - public Layer(ProfileElement parent, string name, int order) : base(parent.Profile) + public Layer(ProfileElement parent, string name) : base(parent, parent.Profile) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Profile = Parent.Profile; Name = name; Suspended = false; @@ -48,7 +45,6 @@ namespace Artemis.Core Adapter = new LayerAdapter(this); Initialize(); - Parent.AddChild(this, order); } /// @@ -57,7 +53,7 @@ namespace Artemis.Core /// The profile the layer belongs to /// The parent of the layer /// The entity of the layer - public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent.Profile) + public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent, parent.Profile) { LayerEntity = layerEntity; EntityId = layerEntity.Id; @@ -148,10 +144,8 @@ namespace Artemis.Core if (LayerBrush?.BaseProperties != null) result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); foreach (BaseLayerEffect layerEffect in LayerEffects) - { if (layerEffect.BaseProperties != null) result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); - } return result; } @@ -172,6 +166,19 @@ namespace Artemis.Core /// public event EventHandler? LayerBrushUpdated; + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + if (LayerBrush?.BrokenState != null) + yield return LayerBrush; + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null)) + yield return baseLayerEffect; + } + + #endregion + /// protected override void Dispose(bool disposing) { @@ -385,6 +392,18 @@ namespace Artemis.Core Enabled = true; } + /// + public override void Activate() + { + throw new NotImplementedException(); + } + + /// + public override void Deactivate() + { + throw new NotImplementedException(); + } + /// public override void Disable() { @@ -512,7 +531,9 @@ namespace Artemis.Core throw new ObjectDisposedException("Layer"); if (!Leds.Any()) + { Path = new SKPath(); + } else { SKPath path = new() {FillType = SKPathFillType.Winding}; @@ -761,19 +782,6 @@ namespace Artemis.Core } #endregion - - #region Overrides of BreakableModel - - /// - public override IEnumerable GetBrokenHierarchy() - { - if (LayerBrush?.BrokenState != null) - yield return LayerBrush; - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null)) - yield return baseLayerEffect; - } - - #endregion } /// diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index a905c3017..ef9dfed2d 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -197,13 +197,9 @@ namespace Artemis.Core // Populate the profile starting at the root, the rest is populated recursively FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId); if (rootFolder == null) - { - Folder _ = new(this, "Root folder", 0); - } + AddChild(new Folder(this, "Root folder")); else - { AddChild(new Folder(this, this, rootFolder)); - } } List renderElements = GetAllRenderElements(); diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 166c3b4b7..f94cdce28 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -156,7 +156,7 @@ namespace Artemis.Core StreamlineOrder(); } - OnChildAdded(); + OnChildAdded(child); } /// @@ -176,7 +176,7 @@ namespace Artemis.Core child.Parent = null; } - OnChildRemoved(); + OnChildRemoved(child); } private void StreamlineOrder() @@ -262,27 +262,27 @@ namespace Artemis.Core /// /// Occurs when a child was added to the list /// - public event EventHandler? ChildAdded; + public event EventHandler? ChildAdded; /// /// Occurs when a child was removed from the list /// - public event EventHandler? ChildRemoved; + public event EventHandler? ChildRemoved; /// /// Invokes the event /// - protected virtual void OnChildAdded() + protected virtual void OnChildAdded(ProfileElement child) { - ChildAdded?.Invoke(this, EventArgs.Empty); + ChildAdded?.Invoke(this, new ProfileElementEventArgs(child)); } /// /// Invokes the event /// - protected virtual void OnChildRemoved() + protected virtual void OnChildRemoved(ProfileElement child) { - ChildRemoved?.Invoke(this, EventArgs.Empty); + ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child)); } #endregion diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 6428c82e4..44066b0db 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -20,12 +20,13 @@ namespace Artemis.Core private SKRectI _bounds; private SKPath? _path; - internal RenderProfileElement(Profile profile) : base(profile) + internal RenderProfileElement(ProfileElement parent, Profile profile) : base(profile) { Timeline = new Timeline(); ExpandedPropertyGroups = new List(); LayerEffectsList = new List(); LayerEffects = new ReadOnlyCollection(LayerEffectsList); + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; @@ -52,6 +53,16 @@ namespace Artemis.Core /// public event EventHandler? LayerEffectsUpdated; + /// + /// Activates the render profile element, loading required brushes, effects or anything else needed for rendering + /// + public abstract void Activate(); + + /// + /// Deactivates the render profile element, disposing required brushes, effects or anything else needed for rendering + /// + public abstract void Deactivate(); + /// protected override void Dispose(bool disposing) { @@ -155,9 +166,9 @@ namespace Artemis.Core /// /// Gets the parent of this element /// - public new ProfileElement? Parent + public new ProfileElement Parent { - get => base.Parent; + get => base.Parent!; internal set { base.Parent = value; diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index d467c25a2..ae3ed6dde 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -137,7 +137,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!SupportsChildren) throw new ArtemisUIException("Cannot add a folder to a profile element of type " + ProfileElement.GetType().Name); - Folder _ = new(ProfileElement, "New folder", 0); + Folder folder = new(ProfileElement, "New folder"); + ProfileElement.AddChild(folder, 0); _profileEditorService.SaveSelectedProfileConfiguration(); } @@ -146,8 +147,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!SupportsChildren) throw new ArtemisUIException("Cannot add a layer to a profile element of type " + ProfileElement.GetType().Name); - Layer layer = new(ProfileElement, "New layer", 0); - + Layer layer = new(ProfileElement, "New layer"); + ProfileElement.AddChild(layer, 0); // Could be null if the default brush got disabled LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); if (brush != null) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs index aa10765c2..9e56b678c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs @@ -90,6 +90,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools private void CreateLayer(Folder folder, List selectedLeds) { Layer newLayer = new(folder, "New layer"); + folder.AddChild(newLayer); LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); if (brush != null) diff --git a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 95b799e45..f2c1ccaf9 100644 --- a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 251ccdd08..29e42c221 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -55,16 +55,12 @@ }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.9, )", - "resolved": "17.1.9", - "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "requested": "[16.3.10, )", + "resolved": "16.3.10", + "contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==", "dependencies": { "DynamicData": "7.4.3", - "Splat": "14.1.1", - "System.ComponentModel": "4.3.0", - "System.Diagnostics.Contracts": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Runtime.Serialization.Primitives": "4.3.0" + "Splat": "13.1.42" } }, "Avalonia.Angle.Windows.Natives": { @@ -923,14 +919,6 @@ "System.Text.Encoding": "4.3.0" } }, - "System.Diagnostics.Contracts": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1411,15 +1399,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1779,7 +1758,7 @@ "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", "RGB.NET.Layout": "1.0.0-prerelease1", - "ReactiveUI": "17.1.9", + "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1", "Splat.Ninject": "14.1.17" } @@ -1797,7 +1776,7 @@ "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", - "ReactiveUI": "17.1.9", + "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index 95b799e45..f2c1ccaf9 100644 --- a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 251ccdd08..29e42c221 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -55,16 +55,12 @@ }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.9, )", - "resolved": "17.1.9", - "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "requested": "[16.3.10, )", + "resolved": "16.3.10", + "contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==", "dependencies": { "DynamicData": "7.4.3", - "Splat": "14.1.1", - "System.ComponentModel": "4.3.0", - "System.Diagnostics.Contracts": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Runtime.Serialization.Primitives": "4.3.0" + "Splat": "13.1.42" } }, "Avalonia.Angle.Windows.Natives": { @@ -923,14 +919,6 @@ "System.Text.Encoding": "4.3.0" } }, - "System.Diagnostics.Contracts": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1411,15 +1399,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1779,7 +1758,7 @@ "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", "RGB.NET.Layout": "1.0.0-prerelease1", - "ReactiveUI": "17.1.9", + "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1", "Splat.Ninject": "14.1.17" } @@ -1797,7 +1776,7 @@ "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", - "ReactiveUI": "17.1.9", + "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 1ceca6a48..8b99e3d4d 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index ef950f083..754b4bc3c 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -93,16 +93,12 @@ }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.9, )", - "resolved": "17.1.9", - "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "requested": "[16.3.10, )", + "resolved": "16.3.10", + "contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==", "dependencies": { "DynamicData": "7.4.3", - "Splat": "14.1.1", - "System.ComponentModel": "4.3.0", - "System.Diagnostics.Contracts": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Runtime.Serialization.Primitives": "4.3.0" + "Splat": "13.1.42" } }, "ReactiveUI.Validation": { @@ -725,8 +721,8 @@ }, "Splat": { "type": "Transitive", - "resolved": "14.1.1", - "contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg==" + "resolved": "13.1.42", + "contentHash": "a/NkGyoSsmvH2YZGgjFxt0dsXkRTgQRMgoUDN8WpBhTUr3wnPTdeQTOLLr2Jc/BCAdOA7cK2+E4Io8I1/q3f3Q==" }, "Svg.Custom": { "type": "Transitive", @@ -889,14 +885,6 @@ "System.Text.Encoding": "4.3.0" } }, - "System.Diagnostics.Contracts": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1377,15 +1365,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 4e2724a5f..8197e5866 100644 --- a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index ce49737fd..b8ac67813 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -82,16 +82,12 @@ }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.9, )", - "resolved": "17.1.9", - "contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==", + "requested": "[16.3.10, )", + "resolved": "16.3.10", + "contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==", "dependencies": { "DynamicData": "7.4.3", - "Splat": "14.1.1", - "System.ComponentModel": "4.3.0", - "System.Diagnostics.Contracts": "4.3.0", - "System.Dynamic.Runtime": "4.3.0", - "System.Runtime.Serialization.Primitives": "4.3.0" + "Splat": "13.1.42" } }, "Avalonia.Angle.Windows.Natives": { @@ -939,14 +935,6 @@ "System.Text.Encoding": "4.3.0" } }, - "System.Diagnostics.Contracts": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1427,15 +1415,6 @@ "System.Runtime.Extensions": "4.3.0" } }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "5.0.0", @@ -1795,7 +1774,7 @@ "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", "RGB.NET.Layout": "1.0.0-prerelease1", - "ReactiveUI": "17.1.9", + "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1", "Splat.Ninject": "14.1.17" } @@ -1813,7 +1792,7 @@ "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease1", - "ReactiveUI": "17.1.9", + "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1" } } diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 44e5e5148..832eb789a 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -23,7 +23,7 @@ - + @@ -59,9 +59,4 @@ - - - ..\..\..\..\HistoricalReactiveCommand\HistoricalReactiveCommand\bin\Debug\netstandard2.1\HistoricalReactiveCommand.dll - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs new file mode 100644 index 000000000..dced97951 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/AddProfileElement.cs @@ -0,0 +1,56 @@ +using System; +using Artemis.Core; +using Artemis.UI.Services.ProfileEditor; + +namespace Artemis.UI.Screens.ProfileEditor.Commands +{ + public class AddProfileElement : IProfileEditorCommand, IDisposable + { + private readonly int _index; + private readonly RenderProfileElement _subject; + private readonly ProfileElement _target; + private bool _isAdded; + + public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index) + { + _subject = subject; + _target = target; + _index = index; + + DisplayName = subject switch + { + Layer => "Add layer", + Folder => "Add folder", + _ => throw new ArgumentException("Type of subject is not supported") + }; + } + + /// + public void Dispose() + { + if (!_isAdded) + _subject.Dispose(); + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName { get; } + + /// + public void Execute() + { + _isAdded = true; + _target.AddChild(_subject, _index); + } + + /// + public void Undo() + { + _isAdded = false; + _target.RemoveChild(_subject); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs new file mode 100644 index 000000000..54257cf61 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Commands/RemoveProfileElement.cs @@ -0,0 +1,63 @@ +using System; +using Artemis.Core; +using Artemis.UI.Exceptions; +using Artemis.UI.Services.ProfileEditor; + +namespace Artemis.UI.Screens.ProfileEditor.Commands +{ + public class RemoveProfileElement : IProfileEditorCommand, IDisposable + { + private readonly int _index; + private readonly RenderProfileElement _subject; + private readonly ProfileElement _target; + private bool _isRemoved; + + public RemoveProfileElement(RenderProfileElement subject) + { + if (subject.Parent == null) + throw new ArtemisUIException("Can't remove a subject that has no parent"); + + _subject = subject; + _target = _subject.Parent; + _index = _subject.Children.IndexOf(_subject); + + DisplayName = subject switch + { + Layer => "Remove layer", + Folder => "Remove folder", + _ => throw new ArgumentException("Type of subject is not supported") + }; + } + + /// + public void Dispose() + { + if (_isRemoved) + _subject.Dispose(); + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName { get; } + + /// + public void Execute() + { + _isRemoved = true; + _target.RemoveChild(_subject); + _target.Deactivate(); + } + + /// + public void Undo() + { + _isRemoved = false; + _target.Activate(); + _target.AddChild(_subject, _index); + + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml index 308d0e7ce..98a327fe4 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml @@ -64,10 +64,27 @@ + + + + + + + + + + + + Command="{Binding Duplicate}" + HotKey="Ctrl+D" + IsEnabled="{Binding HasSelectedElement}"> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs index d3af72a21..153fe97f5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml.cs @@ -2,10 +2,11 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar { - public partial class MenuBarView : UserControl + public partial class MenuBarView : ReactiveUserControl { public MenuBarView() { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index adf871daf..a4927ff55 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -1,13 +1,24 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Reactive.Disposables; +using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar { - public class MenuBarViewModel : ViewModelBase + public class MenuBarViewModel : ActivatableViewModelBase { + private ProfileEditorHistory? _history; + + public MenuBarViewModel(IProfileEditorService profileEditorService) + { + this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); + } + + public ProfileEditorHistory? History + { + get => _history; + set => this.RaiseAndSetIfChanged(ref _history, value); + } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml index 6b4ef1bd6..f45729b40 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml @@ -7,8 +7,8 @@ x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.FolderTreeItemView"> - - - Defines the property. - - - - - Defines the property. - - @@ -239,19 +229,31 @@ - Gets or sets the element that captures input for the selection rectangle. + Gets or sets the element that captures input for the selection rectangle. - + - Gets or sets the command to execute when the selection has been updated. + Occurs when the selection rect is being updated, indicating the user is dragging. - + - Gets or sets the command to execute when the selection has finished. + Occurs when the selection has finished, indicating the user stopped dragging. + + + Invokes the event + + + + + + Invokes the event + + + @@ -783,6 +785,26 @@ If applicable, the previous active profile element before the event was raised + + + Provides data on selection events raised by the . + + + + + Creates a new instance of the class. + + + + + Gets the rectangle that was selected when the event occurred. + + + + + Gets the key modifiers that where pressed when the event occurred. + + Represents errors that occur within the Artemis Shared UI library diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 6296c1e13..fd2e58c98 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index dd8462ae7..5f0bb3e94 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -66,10 +66,12 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[2.80.3, )", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "requested": "[2.88.0-preview.178, )", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, @@ -422,6 +424,16 @@ "Serilog": "2.10.0" } }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "SkiaSharp.Views.Desktop.Common": { "type": "Transitive", "resolved": "2.80.3", @@ -1332,7 +1344,7 @@ "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 5e9f3bdac..30b18815f 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -565,12 +565,24 @@ }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "SkiaSharp.Views.Desktop.Common": { "type": "Transitive", "resolved": "2.80.3", @@ -1479,7 +1491,7 @@ "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", @@ -1506,7 +1518,7 @@ "Ninject.Extensions.Conventions": "3.3.0", "RGB.NET.Core": "1.0.0-prerelease7", "SharpVectors.Reloaded": "1.7.5", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "SkiaSharp.Views.WPF": "2.80.3", "Stylet": "1.3.6", "System.Buffers": "4.5.1", @@ -1522,7 +1534,7 @@ "MaterialDesignThemes": "4.1.0", "Ninject": "3.3.4", "NoStringEvaluating": "2.2.2", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "Stylet": "1.3.6" } } diff --git a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj index 47a179dcc..8d4ac1b0a 100644 --- a/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Artemis.VisualScripting/packages.lock.json b/src/Artemis.VisualScripting/packages.lock.json index a0c2a9431..b70d8a466 100644 --- a/src/Artemis.VisualScripting/packages.lock.json +++ b/src/Artemis.VisualScripting/packages.lock.json @@ -38,10 +38,12 @@ }, "SkiaSharp": { "type": "Direct", - "requested": "[2.80.3, )", - "resolved": "2.80.3", - "contentHash": "qX6tGNP3+MXNYe2pKm0PCRiJ/cx+LTeLaggwZifB7sUMXhECfKKKHJq45VqZKt37xQegnCCdf1jHXwmHeJQs5Q==", + "requested": "[2.88.0-preview.178, )", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", "System.Memory": "4.5.3" } }, @@ -248,23 +250,23 @@ }, "RGB.NET.Core": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" }, "RGB.NET.Layout": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "RGB.NET.Presets": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -408,6 +410,16 @@ "resolved": "1.7.5", "contentHash": "v9U5sSMGFE2zCMbh42BYHkaRYkmwwhsKMGcNRdHAKqD1ryOf4mhqnJR0o07hwg5KIEmCI9bDdrgYSmiZWlL+eA==" }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, "SkiaSharp.Views.Desktop.Common": { "type": "Transitive", "resolved": "2.80.3", @@ -1330,14 +1342,14 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", - "RGB.NET.Presets": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", @@ -1362,9 +1374,9 @@ "Microsoft.Xaml.Behaviors.Wpf": "1.1.31", "Ninject": "3.3.4", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", "SharpVectors.Reloaded": "1.7.5", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "SkiaSharp.Views.WPF": "2.80.3", "Stylet": "1.3.6", "System.Buffers": "4.5.1", diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 29e42c221..7ff439353 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -542,23 +542,23 @@ }, "RGB.NET.Core": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" }, "RGB.NET.Layout": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "RGB.NET.Presets": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -1719,14 +1719,14 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", - "RGB.NET.Presets": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", @@ -1756,10 +1756,11 @@ "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178", "Splat.Ninject": "14.1.17" } }, @@ -1775,9 +1776,10 @@ "Avalonia.Xaml.Interactivity": "0.10.11.5", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", - "ReactiveUI.Validation": "2.2.1" + "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178" } } } diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 29e42c221..7ff439353 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -542,23 +542,23 @@ }, "RGB.NET.Core": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" }, "RGB.NET.Layout": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "RGB.NET.Presets": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -1719,14 +1719,14 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", - "RGB.NET.Presets": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", @@ -1756,10 +1756,11 @@ "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178", "Splat.Ninject": "14.1.17" } }, @@ -1775,9 +1776,10 @@ "Avalonia.Xaml.Interactivity": "0.10.11.5", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", - "ReactiveUI.Validation": "2.2.1" + "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178" } } } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index c6c81a462..42c40c799 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -27,7 +27,8 @@ - + + diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 72ec5f1f0..37ee08bde 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -1,11 +1,10 @@ using System; -using System.Windows.Input; using Artemis.Core; +using Artemis.UI.Shared.Events; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; -using FluentAvalonia.Styling; namespace Artemis.UI.Shared.Controls { @@ -18,15 +17,13 @@ namespace Artemis.UI.Shared.Controls /// Defines the property. /// public static readonly StyledProperty BackgroundProperty = - AvaloniaProperty.Register(nameof(Background), - new SolidColorBrush(AvaloniaLocator.Current.GetService()?.CustomAccentColor ?? Colors.Transparent, 0.25)); + AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25)); /// /// Defines the property. /// public static readonly StyledProperty BorderBrushProperty = - AvaloniaProperty.Register(nameof(BorderBrush), - new SolidColorBrush(AvaloniaLocator.Current.GetService()?.CustomAccentColor ?? Colors.Transparent)); + AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); /// /// Defines the property. @@ -40,18 +37,6 @@ namespace Artemis.UI.Shared.Controls public static readonly StyledProperty InputElementProperty = AvaloniaProperty.Register(nameof(InputElement), notifying: OnInputElementChanged); - /// - /// Defines the property. - /// - public static readonly StyledProperty SelectionUpdatedProperty - = AvaloniaProperty.Register(nameof(SelectionUpdated)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty SelectionFinishedProperty - = AvaloniaProperty.Register(nameof(SelectionUpdated)); - private Rect? _displayRect; private IControl? _oldInputElement; private Point _startPosition; @@ -91,7 +76,7 @@ namespace Artemis.UI.Shared.Controls } /// - /// Gets or sets the element that captures input for the selection rectangle. + /// Gets or sets the element that captures input for the selection rectangle. /// public IControl? InputElement { @@ -100,21 +85,31 @@ namespace Artemis.UI.Shared.Controls } /// - /// Gets or sets the command to execute when the selection has been updated. + /// Occurs when the selection rect is being updated, indicating the user is dragging. /// - public ICommand? SelectionUpdated + public event EventHandler? SelectionUpdated; + + /// + /// Occurs when the selection has finished, indicating the user stopped dragging. + /// + public event EventHandler? SelectionFinished; + + /// + /// Invokes the event + /// + /// + protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e) { - get => GetValue(SelectionUpdatedProperty); - set => SetValue(SelectionUpdatedProperty, value); + SelectionUpdated?.Invoke(this, e); } /// - /// Gets or sets the command to execute when the selection has finished. + /// Invokes the event /// - public ICommand? SelectionFinished + /// + protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e) { - get => GetValue(SelectionFinishedProperty); - set => SetValue(SelectionFinishedProperty, value); + SelectionFinished?.Invoke(this, e); } private static void OnInputElementChanged(IAvaloniaObject sender, bool before) @@ -143,8 +138,7 @@ namespace Artemis.UI.Shared.Controls new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) ); - SelectionUpdated?.Execute(_displayRect.Value); - + OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, e.KeyModifiers)); InvalidateVisual(); } @@ -156,7 +150,7 @@ namespace Artemis.UI.Shared.Controls e.Pointer.Capture(null); if (_displayRect != null) - SelectionFinished?.Execute(_displayRect.Value); + OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, e.KeyModifiers)); _displayRect = null; InvalidateVisual(); @@ -187,7 +181,7 @@ namespace Artemis.UI.Shared.Controls public override void Render(DrawingContext drawingContext) { if (_displayRect != null) - drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value); + drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, 4, 4); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs new file mode 100644 index 000000000..67b33b39a --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs @@ -0,0 +1,32 @@ +using System; +using Artemis.UI.Shared.Controls; +using Avalonia; +using Avalonia.Input; + +namespace Artemis.UI.Shared.Events +{ + /// + /// Provides data on selection events raised by the . + /// + public class SelectionRectangleEventArgs : EventArgs + { + /// + /// Creates a new instance of the class. + /// + public SelectionRectangleEventArgs(Rect rectangle, KeyModifiers keyModifiers) + { + KeyModifiers = keyModifiers; + Rectangle = rectangle; + } + + /// + /// Gets the rectangle that was selected when the event occurred. + /// + public Rect Rectangle { get; } + + /// + /// Gets the key modifiers that where pressed when the event occurred. + /// + public KeyModifiers KeyModifiers { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index 754b4bc3c..6deb8e4f5 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -112,9 +112,20 @@ }, "RGB.NET.Core": { "type": "Direct", - "requested": "[1.0.0-prerelease1, )", - "resolved": "1.0.0-prerelease1", - "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + "requested": "[1.0.0-prerelease7, )", + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" + }, + "SkiaSharp": { + "type": "Direct", + "requested": "[2.88.0-preview.178, )", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", + "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", + "System.Memory": "4.5.3" + } }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", @@ -522,18 +533,18 @@ }, "RGB.NET.Layout": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "RGB.NET.Presets": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -677,16 +688,6 @@ "resolved": "0.5.11", "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, - "SkiaSharp": { - "type": "Transitive", - "resolved": "2.88.0-preview.178", - "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", - "dependencies": { - "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", - "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", - "System.Memory": "4.5.3" - } - }, "SkiaSharp.HarfBuzz": { "type": "Transitive", "resolved": "2.88.0-preview.178", @@ -1685,14 +1686,14 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", - "RGB.NET.Presets": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index b8ac67813..63ff119eb 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -558,23 +558,23 @@ }, "RGB.NET.Core": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" }, "RGB.NET.Layout": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "RGB.NET.Presets": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -1735,14 +1735,14 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", - "RGB.NET.Presets": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", @@ -1772,10 +1772,11 @@ "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178", "Splat.Ninject": "14.1.17" } }, @@ -1791,9 +1792,10 @@ "Avalonia.Xaml.Interactivity": "0.10.11.5", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", - "ReactiveUI.Validation": "2.2.1" + "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178" } } } diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index b3f745f09..def881af2 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -26,8 +26,9 @@ - - + + + diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml.cs index 31652dc1c..27674f033 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml.cs @@ -11,13 +11,6 @@ namespace Artemis.UI.Screens.SurfaceEditor InitializeComponent(); } - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - #region Overrides of InputElement - /// protected override void OnPointerEnter(PointerEventArgs e) { @@ -42,15 +35,9 @@ namespace Artemis.UI.Screens.SurfaceEditor base.OnPointerLeave(e); } - /// - protected override void OnPointerPressed(PointerPressedEventArgs e) + private void InitializeComponent() { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && ViewModel != null) - ViewModel.SelectionStatus = SelectionStatus.Selected; - - base.OnPointerPressed(e); + AvaloniaXamlLoader.Load(this); } - - #endregion } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs index 2e45e3e97..7800ebf5c 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs @@ -99,7 +99,7 @@ namespace Artemis.UI.Screens.SurfaceEditor Device.Y = roundedY; } } - + private void ExecuteIdentifyDevice(ArtemisDevice device) { _deviceService.IdentifyDevice(device); diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml index b22986738..a9535db87 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml @@ -88,7 +88,12 @@ + SelectionUpdated="SelectionRectangle_OnSelectionUpdated" + BorderBrush="{DynamicResource SystemAccentColor}"> + + + + (rgbService.Devices.Select(surfaceVmFactory.SurfaceDeviceViewModel)); - ListDeviceViewModels = new ObservableCollection(rgbService.Devices.Select(surfaceVmFactory.ListDeviceViewModel)); + SurfaceDeviceViewModels = new ObservableCollection(rgbService.Devices.OrderBy(d => d.ZIndex).Select(surfaceVmFactory.SurfaceDeviceViewModel)); + ListDeviceViewModels = new ObservableCollection(rgbService.Devices.OrderBy(d => d.ZIndex).Select(surfaceVmFactory.ListDeviceViewModel)); BringToFront = ReactiveCommand.Create(ExecuteBringToFront); BringForward = ReactiveCommand.Create(ExecuteBringForward); SendToBack = ReactiveCommand.Create(ExecuteSendToBack); SendBackward = ReactiveCommand.Create(ExecuteSendBackward); - - UpdateSelection = ReactiveCommand.Create(ExecuteUpdateSelection); } public ObservableCollection SurfaceDeviceViewModels { get; } @@ -50,8 +45,6 @@ namespace Artemis.UI.Screens.SurfaceEditor public ReactiveCommand SendToBack { get; } public ReactiveCommand SendBackward { get; } - public ReactiveCommand UpdateSelection { get; } - public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value; public void ClearSelection() @@ -62,6 +55,15 @@ namespace Artemis.UI.Screens.SurfaceEditor public void StartMouseDrag(Point mousePosition) { + SurfaceDeviceViewModel? startedOn = GetViewModelAtPoint(mousePosition); + if (startedOn != null && startedOn.SelectionStatus != SelectionStatus.Selected) + { + startedOn.SelectionStatus = SelectionStatus.Selected; + foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels.Where(vm => vm != startedOn)) + device.SelectionStatus = SelectionStatus.None; + + } + foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.StartMouseDrag(mousePosition); } @@ -80,7 +82,6 @@ namespace Artemis.UI.Screens.SurfaceEditor if (_saving) return; - // TODO: Figure out why the UI still locks up here Task.Run(() => { try @@ -95,14 +96,35 @@ namespace Artemis.UI.Screens.SurfaceEditor }); } - private void ExecuteUpdateSelection(Rect rect) + public void SelectFirstDeviceAtPoint(Point point, bool expand) + { + SurfaceDeviceViewModel? toSelect = GetViewModelAtPoint(point); + if (toSelect != null) + toSelect.SelectionStatus = SelectionStatus.Selected; + + if (!expand) + { + foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels.Where(vm => vm != toSelect)) + device.SelectionStatus = SelectionStatus.None; + } + + ApplySurfaceSelection(); + } + + private SurfaceDeviceViewModel? GetViewModelAtPoint(Point point) + { + SKPoint hitTestPoint = point.ToSKPoint(); + return SurfaceDeviceViewModels.OrderByDescending(vm => vm.Device.ZIndex).FirstOrDefault(d => d.Device.Rectangle.Contains(hitTestPoint)); + } + + public void UpdateSelection(Rect rect, bool expand) { SKRect hitTestRect = rect.ToSKRect(); foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels) { if (device.Device.Rectangle.IntersectsWith(hitTestRect)) device.SelectionStatus = SelectionStatus.Selected; - else if (!_inputService.IsKeyDown(KeyboardKey.LeftShift) && !_inputService.IsKeyDown(KeyboardKey.RightShift)) + else if (!expand) device.SelectionStatus = SelectionStatus.None; } diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index 89b7c2801..293dcafa4 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -136,17 +136,28 @@ }, "RGB.NET.Core": { "type": "Direct", - "requested": "[1.0.0-prerelease1, )", - "resolved": "1.0.0-prerelease1", - "contentHash": "aaCPk++kr1TaV/qWYeol5975IN3XifDuuQ9wrCD+nw1cy05BMdGVhuQ72ITb0YRBedssd/btkM51ZABsBd8CEQ==" + "requested": "[1.0.0-prerelease7, )", + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" }, "RGB.NET.Layout": { "type": "Direct", - "requested": "[1.0.0-prerelease1, )", - "resolved": "1.0.0-prerelease1", - "contentHash": "nbaHbcY59tzFSeTDbImhrcR1ZyJpoC0x6WawXdtGXO7x3F91ajM7kM5SJwi/5jHdD61vGV0ARuznmR8ErAWegQ==", + "requested": "[1.0.0-prerelease7, )", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" + } + }, + "SkiaSharp": { + "type": "Direct", + "requested": "[2.88.0-preview.178, )", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", + "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", + "System.Memory": "4.5.3" } }, "Splat.Ninject": { @@ -574,10 +585,10 @@ }, "RGB.NET.Presets": { "type": "Transitive", - "resolved": "1.0.0-prerelease1", - "contentHash": "XU8XeI0fQF26fd0pQHgoe9RaROuvENmZlX/1QyyaN9P3j0LtYmy6ycWZbsXp8byLT0UcGbS+odMiQQAnK+kxgg==", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", "dependencies": { - "RGB.NET.Core": "1.0.0-prerelease1" + "RGB.NET.Core": "1.0.0-prerelease7" } }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { @@ -721,16 +732,6 @@ "resolved": "0.5.11", "contentHash": "a04YHHKRK1xY8ccSgpa6HOmOw9Kuivo2b2qejp9CK00ykdCBK3Mmc+ekBx954+zPQBksN6aLhvn1SEL7QG2s8Q==" }, - "SkiaSharp": { - "type": "Transitive", - "resolved": "2.88.0-preview.178", - "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", - "dependencies": { - "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", - "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", - "System.Memory": "4.5.3" - } - }, "SkiaSharp.HarfBuzz": { "type": "Transitive", "resolved": "2.88.0-preview.178", @@ -1729,14 +1730,14 @@ "Ninject": "3.3.4", "Ninject.Extensions.ChildKernel": "3.3.0", "Ninject.Extensions.Conventions": "3.3.0", - "RGB.NET.Core": "1.0.0-prerelease1", - "RGB.NET.Layout": "1.0.0-prerelease1", - "RGB.NET.Presets": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", "Serilog.Sinks.Console": "4.0.0", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", - "SkiaSharp": "2.80.3", + "SkiaSharp": "2.88.0-preview.178", "System.Buffers": "4.5.1", "System.IO.FileSystem.AccessControl": "5.0.0", "System.Numerics.Vectors": "4.5.0", @@ -1763,9 +1764,10 @@ "Avalonia.Xaml.Interactivity": "0.10.11.5", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", - "RGB.NET.Core": "1.0.0-prerelease1", + "RGB.NET.Core": "1.0.0-prerelease7", "ReactiveUI": "16.3.10", - "ReactiveUI.Validation": "2.2.1" + "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178" } } } From 33c54eaeb0cfc463a3878caa16b5f8a99eb5d0e6 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 8 Jan 2022 17:20:32 +0100 Subject: [PATCH 106/270] Editor - Tweaked profile tree visuals Editor - Add current profile name display, added smooth zooming --- .../Artemis.UI.Avalonia.Shared.xml | 12 ++- .../Controls/SelectionRectangle.cs | 19 ++++- .../Artemis.UI.Shared/Styles/TreeView.axaml | 6 +- .../ProfileTree/FolderTreeItemView.axaml | 5 +- .../ProfileTree/LayerTreeItemView.axaml | 3 +- .../VisualEditor/VisualEditorView.axaml | 77 +++++++++++-------- .../VisualEditor/VisualEditorViewModel.cs | 15 +++- .../SurfaceEditor/SurfaceDeviceView.axaml | 4 +- .../SurfaceEditor/SurfaceEditorView.axaml | 13 +++- 9 files changed, 108 insertions(+), 46 deletions(-) diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 0a46a2c7c..555a46173 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -201,7 +201,12 @@ - Defines the property. + Defines the property. + + + + + Defines the property. @@ -227,6 +232,11 @@ Gets or sets the width of the control's border + + + Gets or sets the radius of the control's border + + Gets or sets the element that captures input for the selection rectangle. diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 37ee08bde..db8d7825a 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -26,11 +26,17 @@ namespace Artemis.UI.Shared.Controls AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); /// - /// Defines the property. + /// Defines the property. /// public static readonly StyledProperty BorderThicknessProperty = AvaloniaProperty.Register(nameof(BorderThickness), 1); + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderRadiusProperty = + AvaloniaProperty.Register(nameof(BorderRadius), 0); + /// /// Defines the property. /// @@ -75,6 +81,15 @@ namespace Artemis.UI.Shared.Controls set => SetValue(BorderThicknessProperty, value); } + /// + /// Gets or sets the radius of the control's border + /// + public double BorderRadius + { + get => GetValue(BorderRadiusProperty); + set => SetValue(BorderRadiusProperty, value); + } + /// /// Gets or sets the element that captures input for the selection rectangle. /// @@ -181,7 +196,7 @@ namespace Artemis.UI.Shared.Controls public override void Render(DrawingContext drawingContext) { if (_displayRect != null) - drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, 4, 4); + drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml index df6fa92d6..8dbe7a74d 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/TreeView.axaml @@ -64,7 +64,7 @@ ColumnDefinitions="Auto, *" Margin="{TemplateBinding Level, Mode=OneWay, Converter={StaticResource TreeViewItemLeftMarginConverter}}"> + Margin="8 0 4 0"> + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml index f45729b40..34bb3b41a 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml @@ -13,16 +13,17 @@ Foreground="White" Background="#E74C4C" BorderBrush="#E74C4C" + Margin="0 0 5 0" Command="{Binding ShowBrokenStateExceptions}"> - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 248a07b0c..2931181fb 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -1,17 +1,28 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; +using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Services; using Artemis.UI.Services.ProfileEditor; using Artemis.UI.Shared; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor { public class VisualEditorViewModel : ActivatableViewModelBase { + private ProfileConfiguration? _profileConfiguration; + public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService) { Devices = new ObservableCollection(rgbService.EnabledDevices); + this.WhenActivated(d => profileEditorService.ProfileConfiguration.Subscribe(configuration => ProfileConfiguration = configuration).DisposeWith(d)); + } + + public ProfileConfiguration? ProfileConfiguration + { + get => _profileConfiguration; + set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value); } public ObservableCollection Devices { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml index 73db3af63..89e01fae6 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml @@ -26,10 +26,10 @@ Classes.selection-border-selected="{Binding Highlighted}" BorderThickness="1"> - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml index a9535db87..66f414663 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml @@ -11,7 +11,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 84456eac9..8234d6a5f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -1,6 +1,9 @@ using System; +using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; @@ -8,13 +11,42 @@ namespace Artemis.UI.Screens.ProfileEditor.MenuBar { public class MenuBarViewModel : ActivatableViewModelBase { + private readonly INotificationService _notificationService; private ProfileEditorHistory? _history; + private Action? _lastMessage; - public MenuBarViewModel(IProfileEditorService profileEditorService) + public MenuBarViewModel(IProfileEditorService profileEditorService, INotificationService notificationService) { + _notificationService = notificationService; this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); + this.WhenAnyValue(x => x.History) + .Select(h => h?.Undo ?? Observable.Never()) + .Switch() + .Subscribe(DisplayUndo); + this.WhenAnyValue(x => x.History) + .Select(h => h?.Redo ?? Observable.Never()) + .Switch() + .Subscribe(DisplayRedo); } + private void DisplayUndo(IProfileEditorCommand? command) + { + if (command == null || History == null) + return; + + _lastMessage?.Invoke(); + _lastMessage = _notificationService.CreateNotification().WithMessage($"Undid '{command.DisplayName}'.").HavingButton(b => b.WithText("Redo").WithCommand(History.Redo)).Show(); + } + + private void DisplayRedo(IProfileEditorCommand? command) + { + if (command == null || History == null) + return; + + _lastMessage?.Invoke(); + _notificationService.CreateNotification().WithMessage($"Redid '{command.DisplayName}'.").HavingButton(b => b.WithText("Undo").WithCommand(History.Undo)).Show(); ; + } + public ProfileEditorHistory? History { get => _history; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml index af78df0b0..9f2443089 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml @@ -14,19 +14,33 @@ + + + - + + + + + + + + - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs index 4a9482d73..8926536a3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml.cs @@ -1,3 +1,4 @@ +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -14,5 +15,11 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree { AvaloniaXamlLoader.Load(this); } + + private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (ViewModel != null) + ViewModel.ProfileElementPropertyGroupViewModel.IsExpanded = !ViewModel.ProfileElementPropertyGroupViewModel.IsExpanded; + } } } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml index 34bb3b41a..393a3f197 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml @@ -25,7 +25,15 @@ Kind="FolderOpen" Margin="0 0 5 0" IsVisible="{Binding IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" /> - + + ViewModel?.Rename.Subscribe(_ => + { + this.Get("Input").Focus(); + this.Get("Input").SelectAll(); + })); + } + + private void InputElement_OnKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + ViewModel?.SubmitRename(); + else if (e.Key == Key.Escape) + ViewModel?.CancelRename(); + } + + private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e) + { + ViewModel?.CancelRename(); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index 5055f6d35..aee6dbd16 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -14,5 +14,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree } public Folder Folder { get; } + + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml index ed528d469..a76dc395c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml @@ -16,7 +16,15 @@ - + + ViewModel?.Rename.Subscribe(_ => + { + this.Get("Input").Focus(); + this.Get("Input").SelectAll(); + })); + } + + private void InputElement_OnKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + ViewModel?.SubmitRename(); + else if (e.Key == Key.Escape) + ViewModel?.CancelRename(); + } + + private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e) + { + ViewModel?.CancelRename(); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml index 192d579d3..95fcc9084 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml @@ -13,11 +13,11 @@ - - - - - + + + + + @@ -35,28 +35,28 @@ - + - + - + - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index d4e730438..802c6a6e4 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using System.Reactive.Disposables; using Artemis.Core; using Artemis.UI.Ninject.Factories; @@ -41,8 +42,10 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree if (model?.ProfileElement is RenderProfileElement renderProfileElement) profileEditorService.ChangeCurrentProfileElement(renderProfileElement); }); - } + + } + public TreeItemViewModel? SelectedChild { get => _selectedChild; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index e3662b8ca..5cc8c6db3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -24,6 +24,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree private bool _isExpanded; private ProfileElement? _profileElement; private RenderProfileElement? _currentProfileElement; + private bool _renaming; + private string? _renameValue; protected TreeItemViewModel(TreeItemViewModel? parent, ProfileElement? profileElement, IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) @@ -51,6 +53,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree profileEditorService.ExecuteCommand(new AddProfileElement(new Folder(ProfileElement, "New folder"), ProfileElement, 0)); }); + Rename = ReactiveCommand.Create(() => + { + Renaming = true; + RenameValue = ProfileElement?.Name; + }); + this.WhenActivated(d => { _profileEditorService.ProfileElement.Subscribe(element => _currentProfileElement = element).DisposeWith(d); @@ -71,11 +79,24 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree set => this.RaiseAndSetIfChanged(ref _isExpanded, value); } + public bool Renaming + { + get => _renaming; + set => this.RaiseAndSetIfChanged(ref _renaming, value); + } + public TreeItemViewModel? Parent { get; set; } public ObservableCollection Children { get; } = new(); public ReactiveCommand AddLayer { get; } public ReactiveCommand AddFolder { get; } + public ReactiveCommand Rename { get; } + + public string? RenameValue + { + get => _renameValue; + set => this.RaiseAndSetIfChanged(ref _renameValue, value); + } public async Task ShowBrokenStateExceptions() { @@ -93,6 +114,23 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree } } + public void SubmitRename() + { + if (ProfileElement == null) + { + Renaming = false; + return; + } + + _profileEditorService.ExecuteCommand(new RenameProfileElement(ProfileElement, RenameValue)); + Renaming = false; + } + + public void CancelRename() + { + Renaming = false; + } + protected void SubscribeToProfileElement(CompositeDisposable d) { if (ProfileElement == null) From bf1aad1549513fd3fb042a9108641279d34f4298 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 10 Jan 2022 21:32:08 +0100 Subject: [PATCH 111/270] Profile editor - Added layer deleting WIP --- .../Services/Builders/NotificationBuilder.cs | 6 +++--- .../Panels/ProfileTree/ProfileTreeView.axaml | 14 +++++++------- .../Panels/ProfileTree/TreeItemViewModel.cs | 7 +++++++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs index 6e14d181f..22fa90f18 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs @@ -209,10 +209,10 @@ public class NotificationButtonBuilder internal IControl Build() { if (_action != null) - return new Button {Content = _text, Command = ReactiveCommand.Create(() => _action())}; + return new Button {Content = _text, Command = ReactiveCommand.Create(() => _action()), Classes = new Classes("AppBarButton")}; if (_command != null) - return new Button {Content = _text, Command = _command, CommandParameter = _commandParameter}; - return new Button {Content = _text}; + return new Button {Content = _text, Command = _command, CommandParameter = _commandParameter, Classes = new Classes("AppBarButton")}; + return new Button {Content = _text, Classes = new Classes("AppBarButton")}; } } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml index 95fcc9084..e79601d7e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml @@ -14,10 +14,10 @@ - - - - + + + + @@ -35,12 +35,12 @@ - + - + @@ -56,7 +56,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 5cc8c6db3..5d16abc25 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -59,6 +59,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree RenameValue = ProfileElement?.Name; }); + Delete = ReactiveCommand.Create(() => + { + if (ProfileElement is RenderProfileElement renderProfileElement) + profileEditorService.ExecuteCommand(new RemoveProfileElement(renderProfileElement)); + }); + this.WhenActivated(d => { _profileEditorService.ProfileElement.Subscribe(element => _currentProfileElement = element).DisposeWith(d); @@ -91,6 +97,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public ReactiveCommand AddLayer { get; } public ReactiveCommand AddFolder { get; } public ReactiveCommand Rename { get; } + public ReactiveCommand Delete { get; } public string? RenameValue { From c04bff1f48d62d48f1b9b7adae4a8cd0f714e683 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 11 Jan 2022 00:25:12 +0100 Subject: [PATCH 112/270] Profile editor - Ported most VMs of the properties tree and timeline --- .../Profile/LayerProperties/LayerProperty.cs | 1 + .../Timeline/TimelinePropertyViewModel.cs | 6 +- .../ProfileEditor/IProfileEditorService.cs | 1 + .../ProfileEditor/ProfileEditorService.cs | 10 + .../PropertyInput/IPropertyInputService.cs | 51 ++++++ .../PropertyInput/PropertyInputService.cs | 171 ++++++++++-------- .../Ninject/Factories/IVMFactory.cs | 7 + .../LayerPropertyViewModelInstanceProvider.cs | 32 ++++ src/Avalonia/Artemis.UI/Ninject/UIModule.cs | 4 + .../ProfileElementPropertyGroupViewModel.cs | 53 +++++- .../ProfileElementPropertyViewModel.cs | 38 +++- .../Timeline/ITimelineKeyframeViewModel.cs | 28 +++ .../Timeline/ITimelinePropertyViewModel.cs | 12 ++ .../Timeline/TimelineEasingViewModel.cs | 50 +++++ .../Timeline/TimelineKeyframeViewModel.cs | 169 +++++++++++++++++ .../Timeline/TimelinePropertyViewModel.cs | 81 +++++++++ .../Tree/ITreePropertyViewModel.cs | 9 + .../Tree/TreeGroupViewModel.cs | 12 +- .../Tree/TreePropertyViewModel.cs | 24 ++- 19 files changed, 657 insertions(+), 102 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/IPropertyInputService.cs create mode 100644 src/Avalonia/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelineKeyframeViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelinePropertyViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineEasingViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineKeyframeViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelinePropertyViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index c89abca04..4ebd5bb61 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -249,6 +249,7 @@ namespace Artemis.Core if (_keyframesEnabled == value) return; _keyframesEnabled = value; OnKeyframesToggled(); + OnPropertyChanged(nameof(KeyframesEnabled)); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs index 3294fec4b..d9ed531d4 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyViewModel.cs @@ -115,8 +115,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline public interface ITimelinePropertyViewModel : IScreen { - List GetAllKeyframeViewModels(); - void WipeKeyframes(TimeSpan? start, TimeSpan? end); - void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount); + List GetAllKeyframeViewModels(); + void WipeKeyframes(TimeSpan? start, TimeSpan? end); + void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index f1f5e2260..b759fe8cc 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -11,6 +11,7 @@ namespace Artemis.UI.Shared.Services.ProfileEditor IObservable ProfileElement { get; } IObservable History { get; } IObservable Time { get; } + IObservable PixelsPerSecond { get; } void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index b78f1b9de..0441f068f 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -15,6 +15,7 @@ internal class ProfileEditorService : IProfileEditorService private readonly Dictionary _profileEditorHistories = new(); private readonly BehaviorSubject _profileElementSubject = new(null); private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero); + private readonly BehaviorSubject _pixelsPerSecondSubject = new(300); private readonly IProfileService _profileService; private readonly IWindowService _windowService; @@ -22,9 +23,12 @@ internal class ProfileEditorService : IProfileEditorService { _profileService = profileService; _windowService = windowService; + ProfileConfiguration = _profileConfigurationSubject.AsObservable(); ProfileElement = _profileElementSubject.AsObservable(); History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); + Time = _timeSubject.AsObservable(); + PixelsPerSecond = _pixelsPerSecondSubject.AsObservable(); } private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration) @@ -43,6 +47,7 @@ internal class ProfileEditorService : IProfileEditorService public IObservable ProfileElement { get; } public IObservable History { get; } public IObservable Time { get; } + public IObservable PixelsPerSecond { get; } public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) { @@ -59,6 +64,11 @@ internal class ProfileEditorService : IProfileEditorService _timeSubject.OnNext(time); } + public void ChangePixelsPerSecond(double pixelsPerSecond) + { + _pixelsPerSecondSubject.OnNext(pixelsPerSecond); + } + public void ExecuteCommand(IProfileEditorCommand command) { try diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/IPropertyInputService.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/IPropertyInputService.cs new file mode 100644 index 000000000..74d6ae76a --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/IPropertyInputService.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.UI.Shared.Services.Interfaces; + +namespace Artemis.UI.Shared.Services.PropertyInput; + +public interface IPropertyInputService : IArtemisSharedUIService +{ + /// + /// Gets a read-only collection of all registered property editors + /// + ReadOnlyCollection RegisteredPropertyEditors { get; } + + /// + /// Registers a new property input view model used in the profile editor for the generic type defined in + /// + /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to + /// + /// + /// + PropertyInputRegistration RegisterPropertyInput(Plugin plugin) where T : PropertyInputViewModel; + + /// + /// Registers a new property input view model used in the profile editor for the generic type defined in + /// + /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to + /// + /// + /// + /// + PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin); + + /// + /// Removes the property input view model + /// + /// + void RemovePropertyInput(PropertyInputRegistration registration); + + /// + /// Determines if there is a matching registration for the provided layer property + /// + /// The layer property to try to find a view model for + bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty); + + /// + /// If a matching registration is found, creates a new supporting + /// + /// + PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty); +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs index 528d7e531..ec23d0850 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputService.cs @@ -2,99 +2,118 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Artemis.Core; -using Artemis.UI.Shared.Services.Interfaces; +using Ninject; +using Ninject.Parameters; -namespace Artemis.UI.Shared.Services.PropertyInput +namespace Artemis.UI.Shared.Services.PropertyInput; + +internal class PropertyInputService : IPropertyInputService { - internal class PropertyInputService : IPropertyInputService + private readonly IKernel _kernel; + private readonly List _registeredPropertyEditors; + + public PropertyInputService(IKernel kernel) { - private readonly List _registeredPropertyEditors; + _kernel = kernel; + _registeredPropertyEditors = new List(); + RegisteredPropertyEditors = new ReadOnlyCollection(_registeredPropertyEditors); + } - public PropertyInputService() + /// + public ReadOnlyCollection RegisteredPropertyEditors { get; } + + public PropertyInputRegistration RegisterPropertyInput(Plugin plugin) where T : PropertyInputViewModel + { + return RegisterPropertyInput(typeof(T), plugin); + } + + public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin) + { + if (!typeof(PropertyInputViewModel).IsAssignableFrom(viewModelType)) + throw new ArtemisSharedUIException($"Property input VM type must implement {nameof(PropertyInputViewModel)}"); + + lock (_registeredPropertyEditors) { - _registeredPropertyEditors = new List(); - RegisteredPropertyEditors = new ReadOnlyCollection(_registeredPropertyEditors); - } + // Indirectly checked if there's a BaseType above + Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0]; + // If the supported type is a generic, assume there is a base type + if (supportedType.IsGenericParameter) + { + if (supportedType.BaseType == null) + throw new ArtemisSharedUIException("Generic property input VM type must have a type constraint"); + supportedType = supportedType.BaseType; + } - /// - public ReadOnlyCollection RegisteredPropertyEditors { get; } + PropertyInputRegistration? existing = _registeredPropertyEditors.FirstOrDefault(r => r.SupportedType == supportedType); + if (existing != null) + { + if (existing.Plugin != plugin) + throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " + + $"registered by {existing.Plugin}"); - /// - public PropertyInputRegistration RegisterPropertyInput(Plugin plugin) where T : PropertyInputViewModel - { - throw new NotImplementedException(); - } + return existing; + } - /// - public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin) - { - throw new NotImplementedException(); - } - - /// - public void RemovePropertyInput(PropertyInputRegistration registration) - { - throw new NotImplementedException(); - } - - /// - public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty) - { - throw new NotImplementedException(); - } - - /// - public PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty) - { - throw new NotImplementedException(); + _kernel.Bind(viewModelType).ToSelf(); + PropertyInputRegistration registration = new(this, plugin, supportedType, viewModelType); + _registeredPropertyEditors.Add(registration); + return registration; } } - public interface IPropertyInputService : IArtemisSharedUIService + public void RemovePropertyInput(PropertyInputRegistration registration) { - /// - /// Gets a read-only collection of all registered property editors - /// - ReadOnlyCollection RegisteredPropertyEditors { get; } + lock (_registeredPropertyEditors) + { + if (_registeredPropertyEditors.Contains(registration)) + { + registration.Unsubscribe(); + _registeredPropertyEditors.Remove(registration); - /// - /// Registers a new property input view model used in the profile editor for the generic type defined in - /// - /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to - /// - /// - /// - PropertyInputRegistration RegisterPropertyInput(Plugin plugin) where T : PropertyInputViewModel; + _kernel.Unbind(registration.ViewModelType); + } + } + } - /// - /// Registers a new property input view model used in the profile editor for the generic type defined in - /// - /// Note: DataBindingProperty will remove itself on plugin disable so you don't have to - /// - /// - /// - /// - PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin); + public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty) + { + PropertyInputRegistration? registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == layerProperty.PropertyType); + if (registration == null && layerProperty.PropertyType.IsEnum) + registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum)); - /// - /// Removes the property input view model - /// - /// - void RemovePropertyInput(PropertyInputRegistration registration); + return registration != null; + } - /// - /// Determines if there is a matching registration for the provided layer property - /// - /// The layer property to try to find a view model for - bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty); + public PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty) + { + Type? viewModelType = null; + PropertyInputRegistration? registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(T)); - /// - /// If a matching registration is found, creates a new supporting - /// - /// - PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty); + // Check for enums if no supported type was found + if (registration == null && typeof(T).IsEnum) + { + // The enum VM will likely be a generic, that requires creating a generic type matching the layer property + registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum)); + if (registration != null && registration.ViewModelType.IsGenericType) + viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments); + } + else if (registration != null) + { + viewModelType = registration.ViewModelType; + } + else + { + return null; + } + + if (viewModelType == null) + return null; + + ConstructorArgument parameter = new("layerProperty", layerProperty); + // ReSharper disable once InconsistentlySynchronizedField + // When you've just spent the last 2 hours trying to figure out a deadlock and reach this line, I'm so, so sorry. I thought this would be fine. + IKernel kernel = registration?.Plugin.Kernel ?? _kernel; + return (PropertyInputViewModel) kernel.Get(viewModelType, parameter); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 0e699c8f6..ffbda5e3a 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -4,6 +4,7 @@ using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.Settings; @@ -75,4 +76,10 @@ namespace Artemis.UI.Ninject.Factories // TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection profileElementPropertyGroups); // TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection profileElementPropertyGroups); } + + public interface IPropertyVmFactory + { + ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel); + ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs b/src/Avalonia/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs new file mode 100644 index 000000000..ab31a993e --- /dev/null +++ b/src/Avalonia/Artemis.UI/Ninject/InstanceProviders/LayerPropertyViewModelInstanceProvider.cs @@ -0,0 +1,32 @@ +using System; +using System.Reflection; +using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Ninject.Extensions.Factory; + +namespace Artemis.UI.Ninject.InstanceProviders +{ + public class LayerPropertyViewModelInstanceProvider : StandardInstanceProvider + { + protected override Type GetType(MethodInfo methodInfo, object[] arguments) + { + if (methodInfo.ReturnType != typeof(ITreePropertyViewModel) && methodInfo.ReturnType != typeof(ITimelinePropertyViewModel)) + return base.GetType(methodInfo, arguments); + + // Find LayerProperty type + Type? layerPropertyType = arguments[0].GetType(); + while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>))) + layerPropertyType = layerPropertyType.BaseType; + if (layerPropertyType == null) + return base.GetType(methodInfo, arguments); + + if (methodInfo.ReturnType == typeof(ITreePropertyViewModel)) + return typeof(TreePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); + if (methodInfo.ReturnType == typeof(ITimelinePropertyViewModel)) + return typeof(TimelinePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments()); + + return base.GetType(methodInfo, arguments); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs index 4461c6a66..af1cfa130 100644 --- a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs +++ b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs @@ -1,11 +1,13 @@ using System; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Ninject.InstanceProviders; using Artemis.UI.Screens; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Avalonia.Platform; using Avalonia.Shared.PlatformSupport; using Ninject.Extensions.Conventions; +using Ninject.Extensions.Factory; using Ninject.Modules; using Ninject.Planning.Bindings.Resolvers; @@ -46,6 +48,8 @@ namespace Artemis.UI.Ninject .BindToFactory(); }); + Kernel.Bind().ToFactory(() => new LayerPropertyViewModelInstanceProvider()); + // Bind all UI services as singletons Kernel.Bind(x => { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs index bd7e5ea2c..4340c49e3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs @@ -1,28 +1,63 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.PropertyInput; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; public class ProfileElementPropertyGroupViewModel : ViewModelBase { + private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; + private readonly IPropertyInputService _propertyInputService; private bool _isVisible; private bool _isExpanded; + private bool _hasChildren; - public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory) + public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService) { - Children = new ObservableCollection(); + _layerPropertyVmFactory = layerPropertyVmFactory; + _propertyInputService = propertyInputService; + Children = new ObservableCollection(); LayerPropertyGroup = layerPropertyGroup; TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this); - IsVisible = !LayerPropertyGroup.IsHidden; - // TODO: Update visiblity on change, can't do it atm because not sure how to unsubscribe from the event + PopulateChildren(); } - public ObservableCollection Children { get; } + private void PopulateChildren() + { + // Get all properties and property groups and create VMs for them + // The group has methods for getting this without reflection but then we lose the order of the properties as they are defined on the group + foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties()) + { + if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute))) + continue; + + if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) + { + ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup); + // Ensure a supported input VM was found, otherwise don't add it + if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value)) + Children.Add(_layerPropertyVmFactory.ProfileElementPropertyViewModel(value)); + } + else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + { + LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup); + if (value != null) + Children.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(value)); + } + } + + HasChildren = Children.Any(i => i is ProfileElementPropertyViewModel {IsVisible: true} || i is ProfileElementPropertyGroupViewModel {IsVisible: true}); + } + + public ObservableCollection Children { get; } public LayerPropertyGroup LayerPropertyGroup { get; } public TreeGroupViewModel TreeGroupViewModel { get; } @@ -37,4 +72,10 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase get => _isExpanded; set => this.RaiseAndSetIfChanged(ref _isExpanded, value); } + + public bool HasChildren + { + get => _hasChildren; + set => this.RaiseAndSetIfChanged(ref _hasChildren, value); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs index da3519ae9..5fa58821a 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs @@ -1,20 +1,44 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Artemis.UI.Shared; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; -public class ProfileElementPropertyViewModel +public class ProfileElementPropertyViewModel : ViewModelBase { - private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; - private readonly IProfileEditorService _profileEditorService; + private bool _isExpanded; + private bool _isHighlighted; + private bool _isVisible; - public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) + public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory) { LayerProperty = layerProperty; - _profileEditorService = profileEditorService; - _layerPropertyVmFactory = layerPropertyVmFactory; + TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this); + TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this); } public ILayerProperty LayerProperty { get; } + public ITreePropertyViewModel TreePropertyViewModel { get; } + public ITimelinePropertyViewModel TimelinePropertyViewModel { get; } + + public bool IsVisible + { + get => _isVisible; + set => this.RaiseAndSetIfChanged(ref _isVisible, value); + } + + public bool IsHighlighted + { + get => _isHighlighted; + set => this.RaiseAndSetIfChanged(ref _isHighlighted, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelineKeyframeViewModel.cs new file mode 100644 index 000000000..8cc49f77b --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelineKeyframeViewModel.cs @@ -0,0 +1,28 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; + +public interface ITimelineKeyframeViewModel +{ + bool IsSelected { get; set; } + TimeSpan Position { get; } + ILayerPropertyKeyframe Keyframe { get; } + + #region Movement + + void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source); + void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source); + void UpdatePosition(TimeSpan position); + void ReleaseMovement(); + + #endregion + + #region Context menu actions + + void PopulateEasingViewModels(); + void ClearEasingViewModels(); + void Delete(bool save = true); + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelinePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelinePropertyViewModel.cs new file mode 100644 index 000000000..21cc94893 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/ITimelinePropertyViewModel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; + +public interface ITimelinePropertyViewModel : IReactiveObject +{ + List GetAllKeyframeViewModels(); + void WipeKeyframes(TimeSpan? start, TimeSpan? end); + void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount); +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineEasingViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineEasingViewModel.cs new file mode 100644 index 000000000..23be99e6a --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineEasingViewModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Artemis.Core; +using Artemis.UI.Shared; +using Avalonia; +using Humanizer; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; + +public class TimelineEasingViewModel : ViewModelBase +{ + private bool _isEasingModeSelected; + + public TimelineEasingViewModel(Easings.Functions easingFunction, bool isSelected) + { + _isEasingModeSelected = isSelected; + + EasingFunction = easingFunction; + Description = easingFunction.Humanize(); + + EasingPoints = new List(); + for (int i = 1; i <= 10; i++) + { + int x = i; + double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10; + EasingPoints.Add(new Point(x, y)); + } + } + + public Easings.Functions EasingFunction { get; } + public List EasingPoints { get; } + public string Description { get; } + + public bool IsEasingModeSelected + { + get => _isEasingModeSelected; + set + { + _isEasingModeSelected = value; + if (value) OnEasingModeSelected(); + } + } + + public event EventHandler EasingModeSelected; + + protected virtual void OnEasingModeSelected() + { + EasingModeSelected?.Invoke(this, EventArgs.Empty); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineKeyframeViewModel.cs new file mode 100644 index 000000000..1bc1eaa7c --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelineKeyframeViewModel.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia.Controls.Mixins; +using DynamicData; +using ReactiveUI; +using Disposable = System.Reactive.Disposables.Disposable; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline +{ + public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineKeyframeViewModel + { + + private bool _isSelected; + private string _timestamp; + private double _x; + private readonly IProfileEditorService _profileEditorService; + + public TimelineKeyframeViewModel(LayerPropertyKeyframe layerPropertyKeyframe, IProfileEditorService profileEditorService) + { + _profileEditorService = profileEditorService; + LayerPropertyKeyframe = layerPropertyKeyframe; + EasingViewModels = new ObservableCollection(); + + this.WhenActivated(d => + { + _profileEditorService.PixelsPerSecond.Subscribe(p => + { + _pixelsPerSecond = p; + _profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d); + Disposable.Create(() => + { + foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels) + timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected; + }).DisposeWith(d); + }).DisposeWith(d); + }); + } + + public LayerPropertyKeyframe LayerPropertyKeyframe { get; } + public ObservableCollection EasingViewModels { get; } + + public double X + { + get => _x; + set => this.RaiseAndSetIfChanged(ref _x, value); + } + + public string Timestamp + { + get => _timestamp; + set => this.RaiseAndSetIfChanged(ref _timestamp, value); + } + + public bool IsSelected + { + get => _isSelected; + set => this.RaiseAndSetIfChanged(ref _isSelected, value); + } + + public TimeSpan Position => LayerPropertyKeyframe.Position; + public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe; + + public void Update() + { + X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds; + Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}"; + } + + + #region Movement + + private TimeSpan? _offset; + private double _pixelsPerSecond; + + public void ReleaseMovement() + { + _offset = null; + } + + public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source) + { + if (source == this) + { + _offset = null; + return; + } + + if (_offset != null) + return; + + _offset = LayerPropertyKeyframe.Position - source.Position; + } + + public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source) + { + if (source == this || _offset == null) + return; + + UpdatePosition(source.Position + _offset.Value); + } + + public void UpdatePosition(TimeSpan position) + { + throw new NotImplementedException(); + + // if (position < TimeSpan.Zero) + // LayerPropertyKeyframe.Position = TimeSpan.Zero; + // else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length) + // LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length; + // else + // LayerPropertyKeyframe.Position = position; + + Update(); + } + + #endregion + + #region Easing + + public void PopulateEasingViewModels() + { + if (EasingViewModels.Any()) + return; + + EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)) + .Cast() + .Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction))); + + foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels) + timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected; + } + + public void ClearEasingViewModels() + { + EasingViewModels.Clear(); + } + + private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e) + { + if (sender is TimelineEasingViewModel timelineEasingViewModel) + SelectEasingMode(timelineEasingViewModel); + } + + public void SelectEasingMode(TimelineEasingViewModel easingViewModel) + { + throw new NotImplementedException(); + + LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction; + // Set every selection to false except on the VM that made the change + foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel)) + propertyTrackEasingViewModel.IsEasingModeSelected = false; + } + + #endregion + + #region Context menu actions + + public void Delete(bool save = true) + { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelinePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelinePropertyViewModel.cs new file mode 100644 index 000000000..63bb39e76 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Timeline/TimelinePropertyViewModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline +{ + public class TimelinePropertyViewModel : ActivatableViewModelBase, ITimelinePropertyViewModel + { + private readonly IProfileEditorService _profileEditorService; + public LayerProperty LayerProperty { get; } + public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; } + public ObservableCollection> KeyframeViewModels { get; } + + public TimelinePropertyViewModel(LayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IProfileEditorService profileEditorService) + { + _profileEditorService = profileEditorService; + LayerProperty = layerProperty; + ProfileElementPropertyViewModel = profileElementPropertyViewModel; + KeyframeViewModels = new ObservableCollection>(); + } + + #region Implementation of ITimelinePropertyViewModel + + public List GetAllKeyframeViewModels() + { + return KeyframeViewModels.Cast().ToList(); + } + + public void WipeKeyframes(TimeSpan? start, TimeSpan? end) + { + start ??= TimeSpan.Zero; + end ??= TimeSpan.MaxValue; + + + List> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList(); + foreach (LayerPropertyKeyframe keyframe in toShift) + LayerProperty.RemoveKeyframe(keyframe); + + UpdateKeyframes(); + } + + public void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount) + { + start ??= TimeSpan.Zero; + end ??= TimeSpan.MaxValue; + + List> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList(); + foreach (LayerPropertyKeyframe keyframe in toShift) + keyframe.Position += amount; + + UpdateKeyframes(); + } + + #endregion + + private void UpdateKeyframes() + { + // Only show keyframes if they are enabled + if (LayerProperty.KeyframesEnabled) + { + List> keyframes = LayerProperty.Keyframes.ToList(); + + List> toRemove = KeyframeViewModels.Where(t => !keyframes.Contains(t.LayerPropertyKeyframe)).ToList(); + foreach (TimelineKeyframeViewModel timelineKeyframeViewModel in toRemove) + KeyframeViewModels.Remove(timelineKeyframeViewModel); + List> toAdd = keyframes.Where(k => KeyframeViewModels.All(t => t.LayerPropertyKeyframe != k)).Select(k => new TimelineKeyframeViewModel(k, _profileEditorService)).ToList(); + foreach (TimelineKeyframeViewModel timelineKeyframeViewModel in toAdd) + KeyframeViewModels.Add(timelineKeyframeViewModel); + } + else + KeyframeViewModels.Clear(); + + foreach (TimelineKeyframeViewModel timelineKeyframeViewModel in KeyframeViewModels) + timelineKeyframeViewModel.Update(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs new file mode 100644 index 000000000..16fcc9eea --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs @@ -0,0 +1,9 @@ +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; + +public interface ITreePropertyViewModel : IReactiveObject +{ + bool HasDataBinding { get; } + double GetDepth(); +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs index dbdd25b59..08286059b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupViewModel.cs @@ -1,9 +1,7 @@ using System; using System.Collections.ObjectModel; -using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Reflection; using System.Threading.Tasks; using Artemis.Core; @@ -24,8 +22,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; public class TreeGroupViewModel : ActivatableViewModelBase { - private readonly IWindowService _windowService; private readonly IProfileEditorService _profileEditorService; + private readonly IWindowService _windowService; private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel; private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel; @@ -41,12 +39,14 @@ public class TreeGroupViewModel : ActivatableViewModelBase ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d); Disposable.Create(CloseViewModels).DisposeWith(d); }); + + // TODO: Update ProfileElementPropertyGroupViewModel visibility on change (can remove the sub on line 41 as well then) } public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; } public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup; - public ObservableCollection? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null; + public ObservableCollection? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null; public LayerPropertyGroupType GroupType { get; private set; } @@ -96,7 +96,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase // Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); ConstructorArgument argument = new(effectParameter.Name!, layerEffect); - EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel)layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); + EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); _effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel); await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel); @@ -138,7 +138,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase _effectConfigurationWindowViewModel?.Close(null); _brushConfigurationWindowViewModel?.Close(null); } - + private void DetermineGroupType() { if (LayerPropertyGroup is LayerGeneralProperties) diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs index 0ca6424eb..63a0c1e8e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs @@ -4,16 +4,32 @@ using Artemis.UI.Shared.Services.PropertyInput; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; -internal class TreePropertyViewModel : ActivatableViewModelBase +internal class TreePropertyViewModel : ActivatableViewModelBase, ITreePropertyViewModel { - public TreePropertyViewModel(LayerProperty layerProperty, ProfileElementPropertyViewModel layerPropertyViewModel, IPropertyInputService propertyInputService) + public TreePropertyViewModel(LayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IPropertyInputService propertyInputService) { LayerProperty = layerProperty; - LayerPropertyViewModel = layerPropertyViewModel; + ProfileElementPropertyViewModel = profileElementPropertyViewModel; PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty); + + // TODO: Update ProfileElementPropertyViewModel visibility on change } public LayerProperty LayerProperty { get; } - public ProfileElementPropertyViewModel LayerPropertyViewModel { get; } + public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; } public PropertyInputViewModel? PropertyInputViewModel { get; } + public bool HasDataBinding => LayerProperty.HasDataBinding; + + public double GetDepth() + { + int depth = 0; + LayerPropertyGroup? current = LayerProperty.LayerPropertyGroup; + while (current != null) + { + depth++; + current = current.Parent; + } + + return depth; + } } \ No newline at end of file From 022beb6a48726fbcd5cad384fcad7dacd575129d Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 12 Jan 2022 22:11:40 +0100 Subject: [PATCH 113/270] Profile editor - Added most properties --- src/Artemis.Core/Extensions/TypeExtensions.cs | 3 +- src/Artemis.Core/Models/Profile/Layer.cs | 6 +- .../Commands/ChangeLayerBrush.cs | 44 ++++++ .../PropertyInput/PropertyInputViewModel.cs | 75 +++++----- .../Artemis.UI.Shared/Styles/TextBox.axaml | 37 ++++- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 22 +-- .../Converters/PropertyTreeMarginConverter.cs | 5 +- .../Display/SKColorDataModelDisplayView.xaml | 64 ++++++++ .../SKColorDataModelDisplayViewModel.cs | 8 + .../PropertyInput/BoolPropertyInputView.axaml | 8 + .../BoolPropertyInputView.axaml.cs | 20 +++ .../BoolPropertyInputViewModel.cs | 13 ++ .../BrushPropertyInputView.axaml | 45 ++++++ .../BrushPropertyInputView.axaml.cs | 20 +++ .../BrushPropertyInputViewModel.cs | 82 +++++++++++ .../ColorGradientPropertyInputView.axaml | 8 + .../ColorGradientPropertyInputView.axaml.cs | 20 +++ .../ColorGradientPropertyInputViewModel.cs | 19 +++ .../PropertyInput/EnumPropertyInputView.axaml | 8 + .../EnumPropertyInputView.axaml.cs | 21 +++ .../EnumPropertyInputViewModel.cs | 17 +++ .../FloatPropertyInputView.axaml | 18 +++ .../FloatPropertyInputView.axaml.cs | 25 ++++ .../FloatPropertyInputViewModel.cs | 20 +++ .../FloatRangePropertyInputView.axaml | 8 + .../FloatRangePropertyInputView.axaml.cs | 20 +++ .../FloatRangePropertyInputViewModel.cs | 68 +++++++++ .../PropertyInput/IntPropertyInputView.axaml | 18 +++ .../IntPropertyInputView.axaml.cs | 25 ++++ .../IntPropertyInputViewModel.cs | 20 +++ .../IntRangePropertyInputView.axaml | 8 + .../IntRangePropertyInputView.axaml.cs | 20 +++ .../IntRangePropertyInputViewModel.cs | 68 +++++++++ .../SKColorPropertyInputView.axaml | 8 + .../SKColorPropertyInputView.axaml.cs | 20 +++ .../SKColorPropertyInputViewModel.cs | 17 +++ .../SKPointPropertyInputView.axaml | 26 ++++ .../SKPointPropertyInputView.axaml.cs | 28 ++++ .../SKPointPropertyInputViewModel.cs | 50 +++++++ .../SKSizePropertyInputView.axaml | 27 ++++ .../SKSizePropertyInputView.axaml.cs | 28 ++++ .../SKSizePropertyInputViewModel.cs | 52 +++++++ .../Panels/MenuBar/MenuBarViewModel.cs | 57 ++------ .../ProfileElementPropertiesView.axaml | 19 ++- .../ProfileElementPropertiesViewModel.cs | 138 +++++++----------- .../ProfileElementPropertyGroupViewModel.cs | 51 ++++--- .../ProfileElementPropertyViewModel.cs | 3 + .../Tree/Dialogs/LayerBrushPresetViewModel.cs | 15 ++ .../Tree/TreeGroupView.axaml | 10 +- .../Tree/TreePropertyView.axaml | 59 +++++++- .../Tree/TreePropertyViewModel.cs | 2 - .../ProfileTree/FolderTreeItemViewModel.cs | 5 +- .../ProfileTree/LayerTreeItemViewModel.cs | 5 +- .../ProfileTree/ProfileTreeViewModel.cs | 7 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 15 +- .../Panels/StatusBar/StatusBarView.axaml | 44 ++++++ .../Panels/StatusBar/StatusBarView.axaml.cs | 18 +++ .../Panels/StatusBar/StatusBarViewModel.cs | 62 ++++++++ .../ProfileEditor/ProfileEditorView.axaml | 8 +- .../ProfileEditor/ProfileEditorViewModel.cs | 6 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 6 + .../Services/RegistrationService.cs | 26 +++- 62 files changed, 1424 insertions(+), 251 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/Dialogs/LayerBrushPresetViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs diff --git a/src/Artemis.Core/Extensions/TypeExtensions.cs b/src/Artemis.Core/Extensions/TypeExtensions.cs index 672c76e15..9a148caef 100644 --- a/src/Artemis.Core/Extensions/TypeExtensions.cs +++ b/src/Artemis.Core/Extensions/TypeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Humanizer; @@ -92,7 +93,7 @@ namespace Artemis.Core /// /// The value to check /// if the value is of a numeric type, otherwise - public static bool IsNumber(this object value) + public static bool IsNumber([NotNullWhenAttribute(true)] this object? value) { return value is sbyte || value is byte diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 90bf0b309..fab80bf52 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -729,7 +729,7 @@ namespace Artemis.Core // Ensure the brush reference matches the brush LayerBrushReference? current = General.BrushReference.BaseValue; if (!descriptor.MatchesLayerBrushReference(current)) - General.BrushReference.BaseValue = new LayerBrushReference(descriptor); + General.BrushReference.SetCurrentValue(new LayerBrushReference(descriptor), null); ActivateLayerBrush(); } @@ -760,6 +760,10 @@ namespace Artemis.Core : null; descriptor?.CreateInstance(this); + General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + OnLayerBrushUpdated(); ClearBrokenState("Failed to initialize layer brush"); } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs new file mode 100644 index 000000000..2032431a6 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs @@ -0,0 +1,44 @@ +using Artemis.Core; +using Artemis.Core.LayerBrushes; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to change the brush of a layer. +/// +public class ChangeLayerBrush : IProfileEditorCommand +{ + private readonly Layer _layer; + private readonly LayerBrushDescriptor _layerBrushDescriptor; + private readonly LayerBrushDescriptor? _previousDescriptor; + + /// + /// Creates a new instance of the class. + /// + public ChangeLayerBrush(Layer layer, LayerBrushDescriptor layerBrushDescriptor) + { + _layer = layer; + _layerBrushDescriptor = layerBrushDescriptor; + _previousDescriptor = layer.LayerBrush?.Descriptor; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Change layer brush"; + + /// + public void Execute() + { + _layer.ChangeLayerBrush(_layerBrushDescriptor); + } + + /// + public void Undo() + { + if (_previousDescriptor != null) + _layer.ChangeLayerBrush(_previousDescriptor); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs index be92954d3..a7a8f18ef 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs @@ -6,6 +6,7 @@ using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Avalonia.Controls.Mixins; using ReactiveUI; +using ReactiveUI.Validation.Helpers; namespace Artemis.UI.Shared.Services.PropertyInput; @@ -15,10 +16,11 @@ namespace Artemis.UI.Shared.Services.PropertyInput; /// The type of property this input view model supports public abstract class PropertyInputViewModel : PropertyInputViewModel { - [AllowNull] - private T _inputValue; - private bool _inputDragging; private T _dragStartValue; + private bool _inputDragging; + + [AllowNull] private T _inputValue; + private TimeSpan _time; /// @@ -52,7 +54,7 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel .DisposeWith(d); }); } - + /// /// Gets the layer property this view model is editing /// @@ -69,7 +71,7 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel public IProfileEditorService ProfileEditorService { get; } /// - /// Gets the property input service + /// Gets the property input service /// public IPropertyInputService PropertyInputService { get; } @@ -89,7 +91,7 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel /// /// Gets or sets the input value /// - [AllowNull] + [MaybeNull] public T InputValue { get => _inputValue; @@ -126,13 +128,6 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _dragStartValue, _time)); } - /// - /// Called when the input value has been applied to the layer property - /// - protected virtual void OnInputValueApplied() - { - } - /// /// Called when the input value has changed /// @@ -147,23 +142,23 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel { } - protected virtual T GetDragStartValue() + /// + /// Called when dragging starts to get the initial value before dragging begun + /// + /// The initial value before dragging begun + protected virtual T? GetDragStartValue() { return InputValue; } /// - /// Applies the input value to the layer property + /// Applies the input value to the layer property using an . /// - protected void ApplyInputValue() + protected virtual void ApplyInputValue() { - OnInputValueChanged(); - LayerProperty.SetCurrentValue(_inputValue, _time); - OnInputValueApplied(); - if (InputDragging) ProfileEditorService.ChangeTime(_time); - else + else if (ValidationContext.IsValid) ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _time)); } @@ -186,23 +181,12 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel this.RaisePropertyChanged(nameof(IsEnabled)); OnDataBindingsChanged(); } - - private void LayerPropertyOnUpdated(object? sender, EventArgs e) - { - UpdateInputValue(); - } - - private void OnDataBindingChange(object? sender, DataBindingEventArgs e) - { - this.RaisePropertyChanged(nameof(IsEnabled)); - OnDataBindingsChanged(); - } } /// /// For internal use only, implement instead. /// -public abstract class PropertyInputViewModel : ActivatableViewModelBase +public abstract class PropertyInputViewModel : ReactiveValidationObject, IActivatableViewModel, IDisposable { /// /// Prevents this type being implemented directly, implement @@ -210,4 +194,29 @@ public abstract class PropertyInputViewModel : ActivatableViewModelBase /// // ReSharper disable once UnusedMember.Global internal abstract object InternalGuard { get; } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } + + #region Implementation of IActivatableViewModel + + /// + public ViewModelActivator Activator { get; } = new(); + + #endregion + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml index acc19236b..5ce52297f 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> @@ -9,12 +10,20 @@ - - - - - - + + + + + + Bluasdadseheh + Bluheheheheh + Bluhgfdgdsheheh + + + Bluasdadseheh + Bluheheheheh + Bluhgfdgdsheheh + @@ -22,7 +31,19 @@ + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 01e3602d7..e9c0b05c0 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -1,4 +1,4 @@ - + Library net6.0 @@ -47,24 +47,4 @@ - - - DebugSettingsView.axaml - - - ProfileElementPropertiesView.axaml - - - BrushConfigurationWindowView.axaml - - - SidebarCategoryEditView.axaml - - - ProfileConfigurationEditView.axaml - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs b/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs index 71aad6a8f..ccc0676ae 100644 --- a/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/PropertyTreeMarginConverter.cs @@ -14,9 +14,8 @@ public class PropertyTreeMarginConverter : IValueConverter { if (value is TreeGroupViewModel treeGroupViewModel) return new Thickness(Length * treeGroupViewModel.GetDepth(), 0, 0, 0); - // TODO - // if (value is ITreePropertyViewModel treePropertyViewModel) - // return new Thickness(Length * treePropertyViewModel.GetDepth(), 0, 0, 0); + if (value is ITreePropertyViewModel treePropertyViewModel) + return new Thickness(Length * treePropertyViewModel.GetDepth(), 0, 0, 0); return new Thickness(0); } diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml b/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml new file mode 100644 index 000000000..e6215d1a6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs new file mode 100644 index 000000000..c08de2224 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs @@ -0,0 +1,8 @@ +using Artemis.UI.Shared.DataModelVisualization; +using SkiaSharp; + +namespace Artemis.UI.DefaultTypes.DataModel.Display; + +public class SKColorDataModelDisplayViewModel : DataModelDisplayViewModel +{ +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml new file mode 100644 index 000000000..84c152c51 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml @@ -0,0 +1,8 @@ + + TODO + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml.cs new file mode 100644 index 000000000..eadaee283 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class BoolPropertyInputView : ReactiveUserControl + { + public BoolPropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs new file mode 100644 index 000000000..60a4fbc8a --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs @@ -0,0 +1,13 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class BoolPropertyInputViewModel : PropertyInputViewModel +{ + public BoolPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml new file mode 100644 index 000000000..adab728e6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml.cs new file mode 100644 index 000000000..53f2e2f1b --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class BrushPropertyInputView : ReactiveUserControl + { + public BrushPropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs new file mode 100644 index 000000000..796b1e439 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.Services; +using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.Dialogs; +using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Artemis.UI.Shared.Services.PropertyInput; +using Avalonia.Controls.Mixins; +using Avalonia.Threading; +using ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class BrushPropertyInputViewModel : PropertyInputViewModel +{ + private readonly IPluginManagementService _pluginManagementService; + private readonly IProfileEditorService _profileEditorService; + private readonly IWindowService _windowService; + private ObservableCollection _descriptors; + + public BrushPropertyInputViewModel(LayerProperty layerProperty, IPluginManagementService pluginManagementService, IWindowService windowService, + IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + _pluginManagementService = pluginManagementService; + _windowService = windowService; + _profileEditorService = profileEditorService; + _descriptors = new ObservableCollection(pluginManagementService.GetFeaturesOfType().SelectMany(l => l.LayerBrushDescriptors)); + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => pluginManagementService.PluginFeatureEnabled += x, x => pluginManagementService.PluginFeatureEnabled -= x) + .Subscribe(e => UpdateDescriptorsIfChanged(e.EventArgs)) + .DisposeWith(d); + Observable.FromEventPattern(x => pluginManagementService.PluginFeatureDisabled += x, x => pluginManagementService.PluginFeatureDisabled -= x) + .Subscribe(e => UpdateDescriptorsIfChanged(e.EventArgs)) + .DisposeWith(d); + }); + } + + public ObservableCollection Descriptors + { + get => _descriptors; + set => this.RaiseAndSetIfChanged(ref _descriptors, value); + } + + public LayerBrushDescriptor? SelectedDescriptor + { + get => Descriptors.FirstOrDefault(d => d.MatchesLayerBrushReference(InputValue)); + set => SetBrushByDescriptor(value); + } + + /// + protected override void ApplyInputValue() + { + if (LayerProperty.ProfileElement is not Layer layer || SelectedDescriptor == null) + return; + + _profileEditorService.ExecuteCommand(new ChangeLayerBrush(layer, SelectedDescriptor)); + if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) + Dispatcher.UIThread.InvokeAsync(() => _windowService.CreateContentDialog().WithViewModel(out LayerBrushPresetViewModel _, ("layerBrush", layer.LayerBrush)).ShowAsync()); + } + + private void UpdateDescriptorsIfChanged(PluginFeatureEventArgs e) + { + if (e.PluginFeature is not LayerBrushProvider) + return; + + Descriptors = new ObservableCollection(_pluginManagementService.GetFeaturesOfType().SelectMany(l => l.LayerBrushDescriptors)); + } + + private void SetBrushByDescriptor(LayerBrushDescriptor? value) + { + if (value != null) + InputValue = new LayerBrushReference(value); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml new file mode 100644 index 000000000..b62cb245a --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml @@ -0,0 +1,8 @@ + + TODO + diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs new file mode 100644 index 000000000..26cf7391d --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class ColorGradientPropertyInputView : ReactiveUserControl + { + public ColorGradientPropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs new file mode 100644 index 000000000..262dd7117 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs @@ -0,0 +1,19 @@ +using System; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class ColorGradientPropertyInputViewModel : PropertyInputViewModel +{ + public ColorGradientPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + } + + public void DialogClosed(object sender, EventArgs e) + { + ApplyInputValue(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml new file mode 100644 index 000000000..c8d1d0490 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml @@ -0,0 +1,8 @@ + + TODO + diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml.cs new file mode 100644 index 000000000..18ec0a5b3 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml.cs @@ -0,0 +1,21 @@ +using Artemis.UI.Shared.Services.PropertyInput; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class EnumPropertyInputView : ReactiveUserControl + { + public EnumPropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputViewModel.cs new file mode 100644 index 000000000..50242616d --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputViewModel.cs @@ -0,0 +1,17 @@ +using System; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class EnumPropertyInputViewModel : PropertyInputViewModel where T : Enum +{ + public EnumPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + // TODO: Test if WhenActivated works here + } + + public Type EnumType => typeof(T); +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml new file mode 100644 index 000000000..a60310ccc --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml.cs new file mode 100644 index 000000000..8a219fb5d --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputView.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class FloatPropertyInputView : ReactiveUserControl +{ + public FloatPropertyInputView() + { + InitializeComponent(); + AddHandler(KeyUpEvent, OnRoutedKeyUp, handledEventsToo: true); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnRoutedKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter || e.Key == Key.Escape) + FocusManager.Instance!.Focus(null); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs new file mode 100644 index 000000000..0a08915ef --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatPropertyInputViewModel.cs @@ -0,0 +1,20 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class FloatPropertyInputViewModel : PropertyInputViewModel +{ + public FloatPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + if (LayerProperty.PropertyDescription.MinInputValue.IsNumber()) + this.ValidationRule(vm => vm.InputValue, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"Value must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + if (LayerProperty.PropertyDescription.MaxInputValue.IsNumber()) + this.ValidationRule(vm => vm.InputValue, i => i < (float) LayerProperty.PropertyDescription.MaxInputValue, + $"Value must be smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml new file mode 100644 index 000000000..2db8defef --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml @@ -0,0 +1,8 @@ + + TODO + diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml.cs new file mode 100644 index 000000000..4602aad94 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class FloatRangePropertyInputView : ReactiveUserControl + { + public FloatRangePropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs new file mode 100644 index 000000000..16df98f6e --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/FloatRangePropertyInputViewModel.cs @@ -0,0 +1,68 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class FloatRangePropertyInputViewModel : PropertyInputViewModel +{ + public FloatRangePropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + this.ValidationRule(vm => vm.Start, start => start <= End, "Start value must be less than the end value."); + this.ValidationRule(vm => vm.End, end => end >= Start, "End value must be greater than the start value."); + + if (LayerProperty.PropertyDescription.MinInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.Start, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"Start value must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + this.ValidationRule(vm => vm.End, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"End value must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + } + + if (LayerProperty.PropertyDescription.MaxInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.Start, i => i < (float) LayerProperty.PropertyDescription.MaxInputValue, + $"Start value must be smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + this.ValidationRule(vm => vm.End, i => i < (float) LayerProperty.PropertyDescription.MaxInputValue, + $"End value must be smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + } + } + + public float Start + { + get => InputValue?.Start ?? 0; + set + { + if (InputValue == null) + InputValue = new FloatRange(value, value + 1); + else + InputValue.Start = value; + + this.RaisePropertyChanged(nameof(Start)); + } + } + + public float End + { + get => InputValue?.End ?? 0; + set + { + if (InputValue == null) + InputValue = new FloatRange(value - 1, value); + else + InputValue.End = value; + + this.RaisePropertyChanged(nameof(End)); + } + } + + + protected override void OnInputValueChanged() + { + this.RaisePropertyChanged(nameof(Start)); + this.RaisePropertyChanged(nameof(End)); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml new file mode 100644 index 000000000..b18fcb3f4 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml.cs new file mode 100644 index 000000000..e58c32f71 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputView.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class IntPropertyInputView : ReactiveUserControl +{ + public IntPropertyInputView() + { + InitializeComponent(); + AddHandler(KeyUpEvent, OnRoutedKeyUp, handledEventsToo: true); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnRoutedKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter || e.Key == Key.Escape) + FocusManager.Instance!.Focus(null); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs new file mode 100644 index 000000000..92f02cfa6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntPropertyInputViewModel.cs @@ -0,0 +1,20 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class IntPropertyInputViewModel : PropertyInputViewModel +{ + public IntPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + if (LayerProperty.PropertyDescription.MinInputValue.IsNumber()) + this.ValidationRule(vm => vm.InputValue, i => i >= (int) LayerProperty.PropertyDescription.MinInputValue, + $"Value must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + if (LayerProperty.PropertyDescription.MaxInputValue.IsNumber()) + this.ValidationRule(vm => vm.InputValue, i => i < (int) LayerProperty.PropertyDescription.MaxInputValue, + $"Value must be smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml new file mode 100644 index 000000000..51785b5fe --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml @@ -0,0 +1,8 @@ + + TODO + diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml.cs new file mode 100644 index 000000000..841670b58 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class IntRangePropertyInputView : ReactiveUserControl + { + public IntRangePropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs new file mode 100644 index 000000000..3e548ceaf --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/IntRangePropertyInputViewModel.cs @@ -0,0 +1,68 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class IntRangePropertyInputViewModel : PropertyInputViewModel +{ + public IntRangePropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + this.ValidationRule(vm => vm.Start, start => start <= End, "Start value must be less than the end value."); + this.ValidationRule(vm => vm.End, end => end >= Start, "End value must be greater than the start value."); + + if (LayerProperty.PropertyDescription.MinInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.Start, i => i >= (int) LayerProperty.PropertyDescription.MinInputValue, + $"Start value must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + this.ValidationRule(vm => vm.End, i => i >= (int)LayerProperty.PropertyDescription.MinInputValue, + $"End value must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + } + + if (LayerProperty.PropertyDescription.MaxInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.Start, i => i < (int)LayerProperty.PropertyDescription.MaxInputValue, + $"Start value must be smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + this.ValidationRule(vm => vm.End, i => i < (int) LayerProperty.PropertyDescription.MaxInputValue, + $"End value must be smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + } + } + + public int Start + { + get => InputValue?.Start ?? 0; + set + { + if (InputValue == null) + InputValue = new IntRange(value, value + 1); + else + InputValue.Start = value; + + this.RaisePropertyChanged(nameof(Start)); + } + } + + public int End + { + get => InputValue?.End ?? 0; + set + { + if (InputValue == null) + InputValue = new IntRange(value - 1, value); + else + InputValue.End = value; + + this.RaisePropertyChanged(nameof(End)); + } + } + + + protected override void OnInputValueChanged() + { + this.RaisePropertyChanged(nameof(Start)); + this.RaisePropertyChanged(nameof(End)); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml new file mode 100644 index 000000000..7ac5dc871 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml @@ -0,0 +1,8 @@ + + TODO + diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs new file mode 100644 index 000000000..95c3e2618 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class SKColorPropertyInputView : ReactiveUserControl + { + public SKColorPropertyInputView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs new file mode 100644 index 000000000..9c17073f5 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputViewModel.cs @@ -0,0 +1,17 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using SkiaSharp; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public class SKColorPropertyInputViewModel : PropertyInputViewModel + { + public SKColorPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml new file mode 100644 index 000000000..9ffc8374f --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml @@ -0,0 +1,26 @@ + + + + + , + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml.cs new file mode 100644 index 000000000..53b09208c --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class SKPointPropertyInputView : ReactiveUserControl + { + public SKPointPropertyInputView() + { + InitializeComponent(); + AddHandler(KeyUpEvent, OnRoutedKeyUp, handledEventsToo: true); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnRoutedKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter || e.Key == Key.Escape) + FocusManager.Instance!.Focus(null); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs new file mode 100644 index 000000000..db5dd9efc --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKPointPropertyInputViewModel.cs @@ -0,0 +1,50 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; +using SkiaSharp; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class SKPointPropertyInputViewModel : PropertyInputViewModel +{ + public SKPointPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + if (LayerProperty.PropertyDescription.MinInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.X, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"X must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + this.ValidationRule(vm => vm.Y, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"Y must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + } + + if (LayerProperty.PropertyDescription.MaxInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.X, i => i <= (float) LayerProperty.PropertyDescription.MaxInputValue, + $"X must be equal to or smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + this.ValidationRule(vm => vm.Y, i => i <= (float) LayerProperty.PropertyDescription.MaxInputValue, + $"Y must be equal to or smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + } + } + + public float X + { + get => InputValue.X; + set => InputValue = new SKPoint(value, Y); + } + + public float Y + { + get => InputValue.Y; + set => InputValue = new SKPoint(X, value); + } + + + protected override void OnInputValueChanged() + { + this.RaisePropertyChanged(nameof(X)); + this.RaisePropertyChanged(nameof(Y)); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml new file mode 100644 index 000000000..02e3f5152 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml @@ -0,0 +1,27 @@ + + + + + , + + + + diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml.cs new file mode 100644 index 000000000..81fabfb40 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputView.axaml.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.DefaultTypes.PropertyInput +{ + public partial class SKSizePropertyInputView : ReactiveUserControl + { + public SKSizePropertyInputView() + { + InitializeComponent(); + AddHandler(KeyUpEvent, OnRoutedKeyUp, handledEventsToo: true); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnRoutedKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter || e.Key == Key.Escape) + FocusManager.Instance!.Focus(null); + } + } +} diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs new file mode 100644 index 000000000..510d93959 --- /dev/null +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKSizePropertyInputViewModel.cs @@ -0,0 +1,52 @@ +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; +using SkiaSharp; + +// using PropertyChanged; + +namespace Artemis.UI.DefaultTypes.PropertyInput; + +public class SKSizePropertyInputViewModel : PropertyInputViewModel +{ + public SKSizePropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) + : base(layerProperty, profileEditorService, propertyInputService) + { + if (LayerProperty.PropertyDescription.MinInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.Width, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"Width must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + this.ValidationRule(vm => vm.Height, i => i >= (float) LayerProperty.PropertyDescription.MinInputValue, + $"Height must be equal to or greater than {LayerProperty.PropertyDescription.MinInputValue}."); + } + + if (LayerProperty.PropertyDescription.MaxInputValue.IsNumber()) + { + this.ValidationRule(vm => vm.Width, i => i <= (float) LayerProperty.PropertyDescription.MaxInputValue, + $"Width must be equal to or smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + this.ValidationRule(vm => vm.Height, i => i <= (float) LayerProperty.PropertyDescription.MaxInputValue, + $"Height must be equal to or smaller than {LayerProperty.PropertyDescription.MaxInputValue}."); + } + } + + // Since SKSize is immutable we need to create properties that replace the SKSize entirely + public float Width + { + get => InputValue.Width; + set => InputValue = new SKSize(value, Height); + } + + public float Height + { + get => InputValue.Height; + set => InputValue = new SKSize(Width, value); + } + + protected override void OnInputValueChanged() + { + this.RaisePropertyChanged(nameof(Width)); + this.RaisePropertyChanged(nameof(Height)); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 8234d6a5f..de0dd23f3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -1,56 +1,23 @@ using System; -using System.Reactive; using System.Reactive.Disposables; -using System.Reactive.Linq; using Artemis.UI.Shared; -using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.MenuBar +namespace Artemis.UI.Screens.ProfileEditor.MenuBar; + +public class MenuBarViewModel : ActivatableViewModelBase { - public class MenuBarViewModel : ActivatableViewModelBase + private ProfileEditorHistory? _history; + + public MenuBarViewModel(IProfileEditorService profileEditorService) { - private readonly INotificationService _notificationService; - private ProfileEditorHistory? _history; - private Action? _lastMessage; + this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); + } - public MenuBarViewModel(IProfileEditorService profileEditorService, INotificationService notificationService) - { - _notificationService = notificationService; - this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); - this.WhenAnyValue(x => x.History) - .Select(h => h?.Undo ?? Observable.Never()) - .Switch() - .Subscribe(DisplayUndo); - this.WhenAnyValue(x => x.History) - .Select(h => h?.Redo ?? Observable.Never()) - .Switch() - .Subscribe(DisplayRedo); - } - - private void DisplayUndo(IProfileEditorCommand? command) - { - if (command == null || History == null) - return; - - _lastMessage?.Invoke(); - _lastMessage = _notificationService.CreateNotification().WithMessage($"Undid '{command.DisplayName}'.").HavingButton(b => b.WithText("Redo").WithCommand(History.Redo)).Show(); - } - - private void DisplayRedo(IProfileEditorCommand? command) - { - if (command == null || History == null) - return; - - _lastMessage?.Invoke(); - _notificationService.CreateNotification().WithMessage($"Redid '{command.DisplayName}'.").HavingButton(b => b.WithText("Undo").WithCommand(History.Undo)).Show(); ; - } - - public ProfileEditorHistory? History - { - get => _history; - set => this.RaiseAndSetIfChanged(ref _history, value); - } + public ProfileEditorHistory? History + { + get => _history; + set => this.RaiseAndSetIfChanged(ref _history, value); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml index ead36d35f..031ee5c09 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml @@ -5,11 +5,16 @@ xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView"> - - - - - - - + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs index ba22901ab..93e518660 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs @@ -21,6 +21,8 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase private readonly IProfileEditorService _profileEditorService; private ProfileElementPropertyGroupViewModel? _brushPropertyGroup; private ObservableAsPropertyHelper? _profileElement; + private readonly Dictionary> _profileElementGroups; + private ObservableCollection _propertyGroupViewModels; /// public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) @@ -28,129 +30,95 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase _profileEditorService = profileEditorService; _layerPropertyVmFactory = layerPropertyVmFactory; PropertyGroupViewModels = new ObservableCollection(); + _profileElementGroups = new Dictionary>(); // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 - this.WhenAnyValue(x => x.ProfileElement) + this.WhenAnyValue(vm => vm.ProfileElement) .Select(p => p is Layer l ? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x) : Observable.Never>()) .Switch() - .Subscribe(_ => ApplyEffects()); - this.WhenAnyValue(x => x.ProfileElement) + .Subscribe(_ => UpdateGroups()); + this.WhenAnyValue(vm => vm.ProfileElement) .Select(p => p != null ? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x) : Observable.Never>()) .Switch() - .Subscribe(_ => ApplyLayerBrush()); - + .Subscribe(_ => UpdateGroups()); // React to service profile element changes as long as the VM is active - this.WhenActivated(d => - { - _profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); - _profileEditorService.ProfileElement.Subscribe(p => PopulateProperties(p)).DisposeWith(d); - }); + + this.WhenActivated(d => _profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d)); + this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups()); } public RenderProfileElement? ProfileElement => _profileElement?.Value; public Layer? Layer => _profileElement?.Value as Layer; - public ObservableCollection PropertyGroupViewModels { get; } - private void PopulateProperties(RenderProfileElement? renderProfileElement) + public ObservableCollection PropertyGroupViewModels { - PropertyGroupViewModels.Clear(); - _brushPropertyGroup = null; + get => _propertyGroupViewModels; + set => this.RaiseAndSetIfChanged(ref _propertyGroupViewModels, value); + } + private void UpdateGroups() + { if (ProfileElement == null) + { + PropertyGroupViewModels.Clear(); return; + } + + if (!_profileElementGroups.TryGetValue(ProfileElement, out List? viewModels)) + { + viewModels = new List(); + _profileElementGroups[ProfileElement] = viewModels; + } + + List groups = new(); - // Add layer root groups if (Layer != null) { - PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.General)); - PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.Transform)); - ApplyLayerBrush(false); + // Add default layer groups + groups.Add(Layer.General); + groups.Add(Layer.Transform); + // Add brush group + if (Layer.LayerBrush?.BaseProperties != null) + groups.Add(Layer.LayerBrush.BaseProperties); } - ApplyEffects(); - } - - private void ApplyLayerBrush(bool sortProperties = true) - { - if (Layer == null) - return; - - bool hideRenderRelatedProperties = Layer.LayerBrush != null && Layer.LayerBrush.SupportsTransformation; - - Layer.General.ShapeType.IsHidden = hideRenderRelatedProperties; - Layer.General.BlendMode.IsHidden = hideRenderRelatedProperties; - Layer.Transform.IsHidden = hideRenderRelatedProperties; - - if (_brushPropertyGroup != null) - { - PropertyGroupViewModels.Remove(_brushPropertyGroup); - _brushPropertyGroup = null; - } - - if (Layer.LayerBrush?.BaseProperties != null) - { - _brushPropertyGroup = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(Layer.LayerBrush.BaseProperties); - PropertyGroupViewModels.Add(_brushPropertyGroup); - } - - if (sortProperties) - SortProperties(); - } - - private void ApplyEffects(bool sortProperties = true) - { - if (ProfileElement == null) - return; - - // Remove VMs of effects no longer applied on the layer - List toRemove = PropertyGroupViewModels - .Where(l => l.LayerPropertyGroup.LayerEffect != null && !ProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) - .ToList(); - foreach (ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel in toRemove) - PropertyGroupViewModels.Remove(profileElementPropertyGroupViewModel); - + // Add effect groups foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects) { - if (PropertyGroupViewModels.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) - continue; - - PropertyGroupViewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerEffect.BaseProperties)); + if (layerEffect.BaseProperties != null) + groups.Add(layerEffect.BaseProperties); } - if (sortProperties) - SortProperties(); - } + // Remove redundant VMs + viewModels.RemoveAll(vm => !groups.Contains(vm.LayerPropertyGroup)); + + // Create VMs for missing groups + foreach (LayerPropertyGroup group in groups) + { + if (viewModels.All(vm => vm.LayerPropertyGroup != group)) + viewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(group)); + } - private void SortProperties() - { // Get all non-effect properties - List nonEffectProperties = PropertyGroupViewModels + List nonEffectProperties = viewModels .Where(l => l.TreeGroupViewModel.GroupType != LayerPropertyGroupType.LayerEffectRoot) .ToList(); // Order the effects - List effectProperties = PropertyGroupViewModels + List effectProperties = viewModels .Where(l => l.TreeGroupViewModel.GroupType == LayerPropertyGroupType.LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) .OrderBy(l => l.LayerPropertyGroup.LayerEffect?.Order) .ToList(); - // Put the non-effect properties in front - for (int index = 0; index < nonEffectProperties.Count; index++) - { - ProfileElementPropertyGroupViewModel layerPropertyGroupViewModel = nonEffectProperties[index]; - if (PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel) != index) - PropertyGroupViewModels.Move(PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel), index); - } + ObservableCollection propertyGroupViewModels = new(); + foreach (ProfileElementPropertyGroupViewModel viewModel in nonEffectProperties) + propertyGroupViewModels.Add(viewModel); + foreach (ProfileElementPropertyGroupViewModel viewModel in effectProperties) + propertyGroupViewModels.Add(viewModel); - // Put the effect properties after, sorted by their order - for (int index = 0; index < effectProperties.Count; index++) - { - ProfileElementPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; - if (PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) - PropertyGroupViewModels.Move(PropertyGroupViewModels.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); - } + PropertyGroupViewModels = propertyGroupViewModels; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs index 4340c49e3..081c3209c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs @@ -15,9 +15,9 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase { private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly IPropertyInputService _propertyInputService; - private bool _isVisible; - private bool _isExpanded; private bool _hasChildren; + private bool _isExpanded; + private bool _isVisible; public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService) { @@ -27,9 +27,34 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase LayerPropertyGroup = layerPropertyGroup; TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this); + // TODO: Centralize visibility updating or do it here and dispose + _isVisible = !LayerPropertyGroup.IsHidden; + PopulateChildren(); } + public ObservableCollection Children { get; } + public LayerPropertyGroup LayerPropertyGroup { get; } + public TreeGroupViewModel TreeGroupViewModel { get; } + + public bool IsVisible + { + get => _isVisible; + set => this.RaiseAndSetIfChanged(ref _isVisible, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + } + + public bool HasChildren + { + get => _hasChildren; + set => this.RaiseAndSetIfChanged(ref _hasChildren, value); + } + private void PopulateChildren() { // Get all properties and property groups and create VMs for them @@ -56,26 +81,4 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase HasChildren = Children.Any(i => i is ProfileElementPropertyViewModel {IsVisible: true} || i is ProfileElementPropertyGroupViewModel {IsVisible: true}); } - - public ObservableCollection Children { get; } - public LayerPropertyGroup LayerPropertyGroup { get; } - public TreeGroupViewModel TreeGroupViewModel { get; } - - public bool IsVisible - { - get => _isVisible; - set => this.RaiseAndSetIfChanged(ref _isVisible, value); - } - - public bool IsExpanded - { - get => _isExpanded; - set => this.RaiseAndSetIfChanged(ref _isExpanded, value); - } - - public bool HasChildren - { - get => _hasChildren; - set => this.RaiseAndSetIfChanged(ref _hasChildren, value); - } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs index 5fa58821a..b7e5305f7 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyViewModel.cs @@ -18,6 +18,9 @@ public class ProfileElementPropertyViewModel : ViewModelBase LayerProperty = layerProperty; TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this); TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this); + + // TODO: Centralize visibility updating or do it here and dispose + _isVisible = !LayerProperty.IsHidden; } public ILayerProperty LayerProperty { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/Dialogs/LayerBrushPresetViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/Dialogs/LayerBrushPresetViewModel.cs new file mode 100644 index 000000000..a379909aa --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/Dialogs/LayerBrushPresetViewModel.cs @@ -0,0 +1,15 @@ +using Artemis.Core.LayerBrushes; +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.Dialogs +{ + public class LayerBrushPresetViewModel : ContentDialogViewModelBase + { + public BaseLayerBrush LayerBrush { get; } + + public LayerBrushPresetViewModel(BaseLayerBrush layerBrush) + { + LayerBrush = layerBrush; + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml index 9f2443089..0ccb5b649 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml @@ -11,24 +11,24 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.TreeGroupView"> - + + Height="29"> - Welcome to Avalonia! + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs index 63a0c1e8e..558013711 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreePropertyViewModel.cs @@ -11,8 +11,6 @@ internal class TreePropertyViewModel : ActivatableViewModelBase, ITreePropert LayerProperty = layerProperty; ProfileElementPropertyViewModel = profileElementPropertyViewModel; PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty); - - // TODO: Update ProfileElementPropertyViewModel visibility on change } public LayerProperty LayerProperty { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index aee6dbd16..cbb530f99 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor; @@ -7,8 +8,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public class FolderTreeItemViewModel : TreeItemViewModel { - public FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder, IWindowService windowService, IProfileEditorService profileEditorService, - IProfileEditorVmFactory profileEditorVmFactory) : base(parent, folder, windowService, profileEditorService, profileEditorVmFactory) + public FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder, IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory, IRgbService rgbService) + : base(parent, folder, windowService, profileEditorService, rgbService, profileEditorVmFactory) { Folder = folder; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index b7bb5f087..ab577c3ce 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor; @@ -7,8 +8,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public class LayerTreeItemViewModel : TreeItemViewModel { - public LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer, IWindowService windowService, IProfileEditorService profileEditorService, - IProfileEditorVmFactory profileEditorVmFactory) : base(parent, layer, windowService, profileEditorService, profileEditorVmFactory) + public LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer, IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory, IRgbService rgbService) + : base(parent, layer, windowService, profileEditorService, rgbService, profileEditorVmFactory) { Layer = layer; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 802c6a6e4..7cec467b0 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reactive; using System.Reactive.Disposables; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor; @@ -16,8 +17,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { private TreeItemViewModel? _selectedChild; - public ProfileTreeViewModel(IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory) - : base(null, null, windowService, profileEditorService, profileEditorVmFactory) + public ProfileTreeViewModel(IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory, IRgbService rgbService) + : base(null, null, windowService, profileEditorService, rgbService, profileEditorVmFactory) { this.WhenActivated(d => { @@ -42,8 +43,6 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree if (model?.ProfileElement is RenderProfileElement renderProfileElement) profileEditorService.ChangeCurrentProfileElement(renderProfileElement); }); - - } public TreeItemViewModel? SelectedChild diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 5d16abc25..7b79ca8b5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; @@ -27,7 +28,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree private bool _renaming; private string? _renameValue; - protected TreeItemViewModel(TreeItemViewModel? parent, ProfileElement? profileElement, IWindowService windowService, IProfileEditorService profileEditorService, + protected TreeItemViewModel(TreeItemViewModel? parent, ProfileElement? profileElement, IWindowService windowService, IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory profileEditorVmFactory) { _windowService = windowService; @@ -40,9 +41,17 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree AddLayer = ReactiveCommand.Create(() => { if (ProfileElement is Layer targetLayer) - profileEditorService.ExecuteCommand(new AddProfileElement(new Layer(targetLayer.Parent, "New layer"), targetLayer.Parent, targetLayer.Order)); + { + Layer layer = new(targetLayer.Parent, "New layer"); + layer.AddLeds(rgbService.EnabledDevices.SelectMany(d => d.Leds)); + profileEditorService.ExecuteCommand(new AddProfileElement(layer, targetLayer.Parent, targetLayer.Order)); + } else if (ProfileElement != null) - profileEditorService.ExecuteCommand(new AddProfileElement(new Layer(ProfileElement, "New layer"), ProfileElement, 0)); + { + Layer layer = new(ProfileElement, "New layer"); + layer.AddLeds(rgbService.EnabledDevices.SelectMany(d => d.Leds)); + profileEditorService.ExecuteCommand(new AddProfileElement(layer, ProfileElement, 0)); + } }); AddFolder = ReactiveCommand.Create(() => diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml new file mode 100644 index 000000000..7c0f57b06 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml.cs new file mode 100644 index 000000000..a171599d5 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.StatusBar +{ + public partial class StatusBarView : ReactiveUserControl + { + public StatusBarView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs new file mode 100644 index 000000000..752d6b36a --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs @@ -0,0 +1,62 @@ +using System; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.StatusBar; + +public class StatusBarViewModel : ActivatableViewModelBase +{ + private ProfileEditorHistory? _history; + private RenderProfileElement? _profileElement; + private string? _statusMessage; + private bool _showStatusMessage; + + public StatusBarViewModel(IProfileEditorService profileEditorService) + { + this.WhenActivated(d => + { + profileEditorService.ProfileElement.Subscribe(p => ProfileElement = p).DisposeWith(d); + profileEditorService.History.Subscribe(history => History = history).DisposeWith(d); + }); + + this.WhenAnyValue(vm => vm.History) + .Select(h => h?.Undo ?? Observable.Never()) + .Switch() + .Subscribe(c => StatusMessage = c != null ? $"Undid '{c.DisplayName}'." : "Nothing to undo."); + this.WhenAnyValue(vm => vm.History) + .Select(h => h?.Redo ?? Observable.Never()) + .Switch() + .Subscribe(c => StatusMessage = c != null ? $"Redid '{c.DisplayName}'." : "Nothing to redo."); + + this.WhenAnyValue(vm => vm.StatusMessage).Subscribe(_ => ShowStatusMessage = true); + this.WhenAnyValue(vm => vm.StatusMessage).Throttle(TimeSpan.FromSeconds(3)).Subscribe(_ => ShowStatusMessage = false); + } + + public RenderProfileElement? ProfileElement + { + get => _profileElement; + set => this.RaiseAndSetIfChanged(ref _profileElement, value); + } + + public ProfileEditorHistory? History + { + get => _history; + set => this.RaiseAndSetIfChanged(ref _history, value); + } + + public string? StatusMessage + { + get => _statusMessage; + set => this.RaiseAndSetIfChanged(ref _statusMessage, value); + } + + public bool ShowStatusMessage + { + get => _showStatusMessage; + set => this.RaiseAndSetIfChanged(ref _showStatusMessage, value); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 37f8b5d90..7a6236639 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -36,6 +36,10 @@ + + + + @@ -62,7 +66,7 @@ - + @@ -80,5 +84,7 @@ Conditions + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 00cfb1863..8d52bfbfd 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -4,6 +4,7 @@ using Artemis.Core; using Artemis.UI.Screens.ProfileEditor.MenuBar; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; using Artemis.UI.Screens.ProfileEditor.ProfileTree; +using Artemis.UI.Screens.ProfileEditor.StatusBar; using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Shared.Services.ProfileEditor; using Ninject; @@ -24,12 +25,14 @@ namespace Artemis.UI.Screens.ProfileEditor ProfileTreeViewModel profileTreeViewModel, ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, MenuBarViewModel menuBarViewModel, - ProfileElementPropertiesViewModel profileElementPropertiesViewModel) + ProfileElementPropertiesViewModel profileElementPropertiesViewModel, + StatusBarViewModel statusBarViewModel) : base(hostScreen, "profile-editor") { VisualEditorViewModel = visualEditorViewModel; ProfileTreeViewModel = profileTreeViewModel; ProfileElementPropertiesViewModel = profileElementPropertiesViewModel; + StatusBarViewModel = statusBarViewModel; if (OperatingSystem.IsWindows()) TitleBarViewModel = profileEditorTitleBarViewModel; @@ -44,6 +47,7 @@ namespace Artemis.UI.Screens.ProfileEditor public ProfileTreeViewModel ProfileTreeViewModel { get; } public MenuBarViewModel? MenuBarViewModel { get; } public ProfileElementPropertiesViewModel ProfileElementPropertiesViewModel { get; } + public StatusBarViewModel StatusBarViewModel { get; } public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public ProfileEditorHistory? History => _history?.Value; diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index 197bb922c..8a35ed42a 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -27,6 +27,7 @@ namespace Artemis.UI.Screens.Root private readonly IDebugService _debugService; private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; private readonly ISettingsService _settingsService; + private readonly IRegistrationService _registrationService; private readonly ISidebarVmFactory _sidebarVmFactory; private readonly IWindowService _windowService; private SidebarViewModel? _sidebarViewModel; @@ -48,6 +49,7 @@ namespace Artemis.UI.Screens.Root _coreService = coreService; _settingsService = settingsService; + _registrationService = registrationService; _windowService = windowService; _debugService = debugService; _assetLoader = assetLoader; @@ -176,6 +178,10 @@ namespace Artemis.UI.Screens.Root /// public void OpenMainWindow() { + _registrationService.RegisterBuiltInDataModelDisplays(); + _registrationService.RegisterBuiltInDataModelInputs(); + _registrationService.RegisterBuiltInPropertyEditors(); + if (_lifeTime.MainWindow == null) { SidebarViewModel = _sidebarVmFactory.SidebarViewModel(this); diff --git a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs index c2681436a..8330d7a00 100644 --- a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs @@ -1,15 +1,21 @@ -using Artemis.Core.Services; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.DefaultTypes.PropertyInput; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Services.PropertyInput; namespace Artemis.UI.Services { public class RegistrationService : IRegistrationService { private readonly IInputService _inputService; + private readonly IPropertyInputService _propertyInputService; + private bool _registeredBuiltInPropertyEditors; - public RegistrationService(IInputService inputService) + public RegistrationService(IInputService inputService, IPropertyInputService propertyInputService) { _inputService = inputService; + _propertyInputService = propertyInputService; } public void RegisterBuiltInDataModelDisplays() { @@ -21,6 +27,22 @@ namespace Artemis.UI.Services public void RegisterBuiltInPropertyEditors() { + if (_registeredBuiltInPropertyEditors) + return; + + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(typeof(EnumPropertyInputViewModel<>), Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + _propertyInputService.RegisterPropertyInput(Constants.CorePlugin); + + _registeredBuiltInPropertyEditors = true; } public void RegisterControllers() From 1832a25426e3fce72ed2175766bd46c24746c102 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 16 Jan 2022 00:46:19 +0100 Subject: [PATCH 114/270] Core - Reworked brush/effect property storage Profile editor - Added brush selection Profile editor - Added playback controls --- src/Artemis.Core/Models/Profile/Layer.cs | 83 +++----- .../PropertyDescriptionAttribute.cs | 5 + .../PropertyGroupDescriptionAttribute.cs | 11 +- .../Profile/LayerProperties/ILayerProperty.cs | 11 +- .../Profile/LayerProperties/LayerProperty.cs | 20 +- .../Models/Profile/LayerPropertyGroup.cs | 120 ++++++----- .../Models/Profile/RenderProfileElement.cs | 20 +- .../LayerBrushes/Internal/BaseLayerBrush.cs | 14 ++ .../Internal/PropertiesLayerBrush.cs | 8 +- .../Plugins/LayerBrushes/LayerBrush.cs | 2 +- .../LayerBrushes/LayerBrushDescriptor.cs | 16 +- .../Plugins/LayerBrushes/PerLedLayerBrush.cs | 2 +- .../LayerEffects/Internal/BaseLayerEffect.cs | 24 ++- .../Plugins/LayerEffects/LayerEffect.cs | 3 +- .../LayerEffects/LayerEffectDescriptor.cs | 30 ++- .../Placeholder/PlaceholderLayerEffect.cs | 2 +- .../Entities/Profile/LayerBrushEntity.cs | 11 + .../Entities/Profile/LayerEffectEntity.cs | 3 + .../Entities/Profile/LayerEntity.cs | 5 +- .../Entities/Profile/PropertyEntity.cs | 24 +-- .../Entities/Profile/PropertyGroupEntity.cs | 10 + .../BrushPropertyInputViewModel.cs | 14 +- .../LayerPropertiesViewModel.cs | 50 ++--- .../LayerPropertyGroupViewModel.cs | 4 +- .../Timeline/Models/KeyframeClipboardModel.cs | 20 +- .../Tree/TreeGroupViewModel.cs | 180 ++++++++-------- .../ProfileTree/TreeItem/TreeItemViewModel.cs | 4 +- .../Tools/SelectionToolViewModel.cs | 4 +- .../Commands/ChangeLayerBrush.cs | 36 +++- .../ProfileEditor/IProfileEditorService.cs | 94 +++++++-- .../ProfileEditor/ProfileEditorService.cs | 92 ++++++++- .../Artemis.UI.Shared/Styles/Artemis.axaml | 4 +- .../Styles/{TextBox.axaml => Condensed.axaml} | 33 ++- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 1 + .../Assets/Images/Logo/application.ico | Bin 0 -> 113388 bytes .../Converters/SKColorToColor2Converter.cs | 29 +++ .../Converters/SKColorToStringConverter.cs | 28 +++ .../PropertyInput/BoolPropertyInputView.axaml | 3 +- .../BoolPropertyInputViewModel.cs | 20 +- .../BrushPropertyInputView.axaml | 2 +- .../BrushPropertyInputViewModel.cs | 10 + .../PropertyInput/EnumPropertyInputView.axaml | 5 +- .../SKColorPropertyInputView.axaml | 28 ++- src/Avalonia/Artemis.UI/MainWindow.axaml | 2 +- .../Ninject/Factories/IVMFactory.cs | 4 + .../Screens/Debugger/DebugView.axaml | 2 +- .../Screens/Device/DevicePropertiesView.axaml | 2 +- .../Plugins/PluginSettingsWindowView.axaml | 2 +- .../Panels/Playback/PlaybackView.axaml | 63 ++++++ .../Panels/Playback/PlaybackView.axaml.cs | 18 ++ .../Panels/Playback/PlaybackViewModel.cs | 193 ++++++++++++++++++ .../ProfileElementPropertiesView.axaml | 49 ++++- .../ProfileElementPropertiesView.axaml.cs | 25 ++- .../ProfileElementPropertiesViewModel.cs | 92 ++++----- .../ProfileElementPropertyGroupViewModel.cs | 17 ++ .../Tree/ITreePropertyViewModel.cs | 4 +- .../Tree/TreeGroupView.axaml | 32 +-- .../Tree/TreeGroupViewModel.cs | 23 ++- .../Tree/TreePropertyView.axaml.cs | 18 +- .../Tree/TreePropertyViewModel.cs | 2 + .../ProfileEditor/ProfileEditorView.axaml | 10 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 2 +- .../Artemis.UI/Screens/Root/SplashView.axaml | 2 +- .../ProfileConfigurationEditView.axaml | 2 +- 64 files changed, 1168 insertions(+), 481 deletions(-) create mode 100644 src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs create mode 100644 src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs rename src/Avalonia/Artemis.UI.Shared/Styles/{TextBox.axaml => Condensed.axaml} (61%) create mode 100644 src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico create mode 100644 src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs create mode 100644 src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index fab80bf52..4eadc28c6 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -94,7 +94,7 @@ namespace Artemis.Core /// /// Gets the general properties of the layer /// - [PropertyGroupDescription(Name = "General", Description = "A collection of general properties")] + [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")] public LayerGeneralProperties General { get => _general; @@ -104,7 +104,7 @@ namespace Artemis.Core /// /// Gets the transform properties of the layer /// - [PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")] + [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")] public LayerTransformProperties Transform { get => _transform; @@ -208,19 +208,20 @@ namespace Artemis.Core LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; // Layers have two hardcoded property groups, instantiate them - Attribute generalAttribute = Attribute.GetCustomAttribute( + PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( GetType().GetProperty(nameof(General))!, typeof(PropertyGroupDescriptionAttribute) )!; - Attribute transformAttribute = Attribute.GetCustomAttribute( + PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( GetType().GetProperty(nameof(Transform))!, typeof(PropertyGroupDescriptionAttribute) )!; - General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; - General.Initialize(this, "General.", Constants.CorePluginFeature); - Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute; - Transform.Initialize(this, "Transform.", Constants.CorePluginFeature); + LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier}; + LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier}; + + General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup); + Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup); General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; ApplyShapeType(); @@ -241,8 +242,7 @@ namespace Artemis.Core return; LayerBrushReference? current = General.BrushReference.CurrentValue; - if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && - e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) ActivateLayerBrush(); } @@ -282,7 +282,13 @@ namespace Artemis.Core General.ApplyToEntity(); Transform.ApplyToEntity(); - LayerBrush?.BaseProperties?.ApplyToEntity(); + + // Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush + if (LayerBrush != null) + { + LayerBrush.Save(); + LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity; + } // LEDs LayerEntity.Leds.Clear(); @@ -712,53 +718,32 @@ namespace Artemis.Core #region Brush management /// - /// Changes the current layer brush to the brush described in the provided + /// Changes the current layer brush to the provided layer brush and activates it /// - public void ChangeLayerBrush(LayerBrushDescriptor descriptor) + public void ChangeLayerBrush(BaseLayerBrush? layerBrush) { - if (descriptor == null) - throw new ArgumentNullException(nameof(descriptor)); + General.BrushReference.SetCurrentValue(layerBrush != null ? new LayerBrushReference(layerBrush.Descriptor) : null, null); + LayerBrush = layerBrush; if (LayerBrush != null) - { - BaseLayerBrush brush = LayerBrush; - LayerBrush = null; - brush.Dispose(); - } - - // Ensure the brush reference matches the brush - LayerBrushReference? current = General.BrushReference.BaseValue; - if (!descriptor.MatchesLayerBrushReference(current)) - General.BrushReference.SetCurrentValue(new LayerBrushReference(descriptor), null); - - ActivateLayerBrush(); - } - - /// - /// Removes the current layer brush from the layer - /// - public void RemoveLayerBrush() - { - if (LayerBrush == null) - return; - - BaseLayerBrush brush = LayerBrush; - DeactivateLayerBrush(); - LayerEntity.PropertyEntities.RemoveAll(p => p.FeatureId == brush.ProviderId && p.Path.StartsWith("LayerBrush.")); + ActivateLayerBrush(); + else + OnLayerBrushUpdated(); } internal void ActivateLayerBrush() { try { - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (current == null) + if (LayerBrush == null) + { + // If the brush is null, try to instantiate it + LayerBrushReference? brushReference = General.BrushReference.CurrentValue; + if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null) + ChangeLayerBrush(LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush)); + // If that's not possible there's nothing to do return; - - LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null - ? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor - : null; - descriptor?.CreateInstance(this); + } General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; @@ -778,9 +763,9 @@ namespace Artemis.Core if (LayerBrush == null) return; - BaseLayerBrush brush = LayerBrush; + BaseLayerBrush? brush = LayerBrush; LayerBrush = null; - brush.Dispose(); + brush?.Dispose(); OnLayerBrushUpdated(); } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs index e4b63a567..b3b58b7bb 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs @@ -7,6 +7,11 @@ namespace Artemis.Core /// public class PropertyDescriptionAttribute : Attribute { + /// + /// The identifier of this property used for storage, if not set one will be generated property name in code + /// + public string? Identifier { get; set; } + /// /// The user-friendly name for this property, shown in the UI /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs index 98cdb74db..4049688b6 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs @@ -8,12 +8,17 @@ namespace Artemis.Core public class PropertyGroupDescriptionAttribute : Attribute { /// - /// The user-friendly name for this property, shown in the UI. + /// The identifier of this property group used for storage, if not set one will be generated based on the group name in code /// - public string? Name { get; set; } + public string? Identifier { get; set; } /// - /// The user-friendly description for this property, shown in the UI. + /// The user-friendly name for this property group, shown in the UI. + /// + public string? Name { get; set; } + + /// + /// The user-friendly description for this property group, shown in the UI. /// public string? Description { get; set; } } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 52ae56bf1..9a4fd8333 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -16,6 +16,11 @@ namespace Artemis.Core /// Gets the description attribute applied to this property /// PropertyDescriptionAttribute PropertyDescription { get; } + + /// + /// Gets the profile element (such as layer or folder) this property is applied to + /// + RenderProfileElement ProfileElement { get; } /// /// The parent group of this layer property, set after construction @@ -43,7 +48,7 @@ namespace Artemis.Core public bool DataBindingsSupported { get; } /// - /// Gets the unique path of the property on the layer + /// Gets the unique path of the property on the render element /// string Path { get; } @@ -56,7 +61,7 @@ namespace Artemis.Core /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// bool IsLoadedFromStorage { get; } - + /// /// Initializes the layer property /// @@ -64,7 +69,7 @@ namespace Artemis.Core /// /// /// - void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); /// /// Attempts to load and add the provided keyframe entity to the layer property diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 4ebd5bb61..9cfb5769d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Text; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -27,10 +28,10 @@ namespace Artemis.Core // These are set right after construction to keep the constructor (and inherited constructs) clean ProfileElement = null!; LayerPropertyGroup = null!; - Path = null!; Entity = null!; PropertyDescription = null!; DataBinding = null!; + Path = ""; CurrentValue = default!; DefaultValue = default!; @@ -117,13 +118,11 @@ namespace Artemis.Core } } - /// - /// Gets the profile element (such as layer or folder) this property is applied to - /// - public RenderProfileElement ProfileElement { get; internal set; } + /// + public RenderProfileElement ProfileElement { get; private set; } /// - public LayerPropertyGroup LayerPropertyGroup { get; internal set; } + public LayerPropertyGroup LayerPropertyGroup { get; private set; } #endregion @@ -457,16 +456,18 @@ namespace Artemis.Core internal PropertyEntity Entity { get; set; } /// - public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path) + public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); + if (description.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); + _isInitialized = true; ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); - Path = path; Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; @@ -475,6 +476,9 @@ namespace Artemis.Core if (PropertyDescription.DisableKeyframes) KeyframesSupported = false; + // Create the path to this property by walking up the tree + Path = LayerPropertyGroup.Path + "." + description.Identifier; + OnInitialize(); } diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index 130f06e87..3cdbeec17 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -31,9 +31,7 @@ namespace Artemis.Core { // These are set right after construction to keep the constructor (and inherited constructs) clean GroupDescription = null!; - Feature = null!; - ProfileElement = null!; - Path = null!; + Path = ""; _layerProperties = new List(); _layerPropertyGroups = new List(); @@ -42,47 +40,32 @@ namespace Artemis.Core LayerPropertyGroups = new ReadOnlyCollection(_layerPropertyGroups); } - /// - /// Gets the description of this group - /// - public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } - - /// - /// Gets the plugin feature this group is associated with - /// - public PluginFeature Feature { get; set; } - /// /// Gets the profile element (such as layer or folder) this group is associated with /// - public RenderProfileElement ProfileElement { get; internal set; } + public RenderProfileElement ProfileElement { get; private set; } + + /// + /// Gets the description of this group + /// + public PropertyGroupDescriptionAttribute GroupDescription { get; private set; } /// /// The parent group of this group /// - [LayerPropertyIgnore] + [LayerPropertyIgnore] // Ignore the parent when selecting child groups public LayerPropertyGroup? Parent { get; internal set; } /// - /// The path of this property group + /// Gets the unique path of the property on the render element /// - public string Path { get; internal set; } + public string Path { get; private set; } /// /// Gets whether this property groups properties are all initialized /// public bool PropertiesInitialized { get; private set; } - /// - /// The layer brush this property group belongs to - /// - public BaseLayerBrush? LayerBrush { get; internal set; } - - /// - /// The layer effect this property group belongs to - /// - public BaseLayerEffect? LayerEffect { get; internal set; } - /// /// Gets or sets whether the property is hidden in the UI /// @@ -96,6 +79,11 @@ namespace Artemis.Core } } + /// + /// Gets the entity this property group uses for persistent storage + /// + public PropertyGroupEntity? PropertyGroupEntity { get; internal set; } + /// /// A list of all layer properties in this group /// @@ -194,17 +182,20 @@ namespace Artemis.Core } } - internal void Initialize(RenderProfileElement profileElement, string path, PluginFeature feature) + internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity) { - if (path == null) throw new ArgumentNullException(nameof(path)); + if (groupDescription.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); // Doubt this will happen but let's make sure if (PropertiesInitialized) throw new ArtemisCoreException("Layer property group already initialized, wut"); - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); - ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); - Path = path.TrimEnd('.'); + ProfileElement = profileElement; + Parent = parent; + GroupDescription = groupDescription; + PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier}; + Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier; // Get all properties implementing ILayerProperty or LayerPropertyGroup foreach (PropertyInfo propertyInfo in GetType().GetProperties()) @@ -271,61 +262,68 @@ namespace Artemis.Core private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { - string path = $"{Path}.{propertyInfo.Name}"; - - if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path}"); - - if (!(Activator.CreateInstance(propertyInfo.PropertyType, true) is ILayerProperty instance)) - throw new ArtemisPluginException($"Failed to create instance of layer property at {path}"); - - // Ensure the description has a name, if not this is a good point to set it based on the property info + // Ensure the description has an identifier and name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyDescription.Identifier)) + propertyDescription.Identifier = propertyInfo.Name; if (string.IsNullOrWhiteSpace(propertyDescription.Name)) propertyDescription.Name = propertyInfo.Name.Humanize(); - PropertyEntity entity = GetPropertyEntity(ProfileElement, path, out bool fromStorage); - instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription, path); + if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}"); + if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance) + throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}"); + + PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage); + instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription); propertyInfo.SetValue(this, instance); + _layerProperties.Add(instance); } private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription) { - string path = Path + "."; - - if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); - - if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance)) - throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}"); - - // Ensure the description has a name, if not this is a good point to set it based on the property info + // Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier)) + propertyGroupDescription.Identifier = propertyInfo.Name; if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name)) propertyGroupDescription.Name = propertyInfo.Name.Humanize(); - instance.Parent = this; - instance.GroupDescription = propertyGroupDescription; - instance.LayerBrush = LayerBrush; - instance.LayerEffect = LayerEffect; - instance.Initialize(ProfileElement, $"{path}{propertyInfo.Name}.", Feature); + if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}"); + if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance)) + throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}"); + + PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier); + instance.Initialize(ProfileElement, this, propertyGroupDescription, entity); propertyInfo.SetValue(this, instance); _layerPropertyGroups.Add(instance); } - private PropertyEntity GetPropertyEntity(RenderProfileElement profileElement, string path, out bool fromStorage) + private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage) { - PropertyEntity? entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.FeatureId == Feature.Id && p.Path == path); + if (PropertyGroupEntity == null) + throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup"); + + PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier); fromStorage = entity != null; if (entity == null) { - entity = new PropertyEntity {FeatureId = Feature.Id, Path = path}; - profileElement.RenderElementEntity.PropertyEntities.Add(entity); + entity = new PropertyEntity {Identifier = identifier}; + PropertyGroupEntity.Properties.Add(entity); } return entity; } + private PropertyGroupEntity GetPropertyGroupEntity(string identifier) + { + if (PropertyGroupEntity == null) + throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup"); + + return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity() {Identifier = identifier}; + } + /// public void Dispose() { diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 44066b0db..be2ec4a2b 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -97,20 +97,10 @@ namespace Artemis.Core internal void SaveRenderElement() { RenderElementEntity.LayerEffects.Clear(); - foreach (BaseLayerEffect layerEffect in LayerEffects) + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) { - LayerEffectEntity layerEffectEntity = new() - { - Id = layerEffect.EntityId, - ProviderId = layerEffect.Descriptor?.PlaceholderFor ?? layerEffect.ProviderId, - EffectType = layerEffect.GetEffectTypeName(), - Name = layerEffect.Name, - Suspended = layerEffect.Suspended, - HasBeenRenamed = layerEffect.HasBeenRenamed, - Order = layerEffect.Order - }; - RenderElementEntity.LayerEffects.Add(layerEffectEntity); - layerEffect.BaseProperties?.ApplyToEntity(); + baseLayerEffect.Save(); + RenderElementEntity.LayerEffects.Add(baseLayerEffect.LayerEffectEntity); } // Condition @@ -310,7 +300,7 @@ namespace Artemis.Core foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects) { // If there is a non-placeholder existing effect, skip this entity - BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.EntityId == layerEffectEntity.Id); + BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.LayerEffectEntity.Id == layerEffectEntity.Id); if (existing != null && existing.Descriptor.PlaceholderFor == null) continue; @@ -351,7 +341,7 @@ namespace Artemis.Core List pluginEffects = LayerEffectsList.Where(ef => ef.ProviderId == e.Registration.PluginFeature.Id).ToList(); foreach (BaseLayerEffect pluginEffect in pluginEffects) { - LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.EntityId); + LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.LayerEffectEntity.Id); LayerEffectsList.Remove(pluginEffect); pluginEffect.Dispose(); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 40a37b09d..69027def6 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.LayerBrushes @@ -24,6 +25,7 @@ namespace Artemis.Core.LayerBrushes // Both are set right after construction to keep the constructor of inherited classes clean _layer = null!; _descriptor = null!; + LayerBrushEntity = null!; } /// @@ -35,6 +37,11 @@ namespace Artemis.Core.LayerBrushes internal set => SetAndNotify(ref _layer, value); } + /// + /// Gets the brush entity this brush uses for persistent storage + /// + public LayerBrushEntity LayerBrushEntity { get; internal set; } + /// /// Gets the descriptor of this brush /// @@ -185,6 +192,13 @@ namespace Artemis.Core.LayerBrushes public override string BrokenDisplayName => Descriptor.DisplayName; #endregion + + internal void Save() + { + // No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change + BaseProperties?.ApplyToEntity(); + LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 4225b0455..fca826e2d 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; namespace Artemis.Core.LayerBrushes { @@ -32,12 +33,11 @@ namespace Artemis.Core.LayerBrushes internal set => _properties = value; } - internal void InitializeProperties() + internal void InitializeProperties(PropertyGroupEntity? propertyGroupEntity) { Properties = Activator.CreateInstance(); - Properties.GroupDescription = new PropertyGroupDescriptionAttribute {Name = Descriptor.DisplayName, Description = Descriptor.Description}; - Properties.LayerBrush = this; - Properties.Initialize(Layer, "LayerBrush.", Descriptor.Provider); + PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description}; + Properties.Initialize(Layer, null, groupDescription, propertyGroupEntity); PropertiesInitialized = true; EnableLayerBrush(); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs index 9e69380d0..749d64bfa 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs @@ -33,7 +33,7 @@ namespace Artemis.Core.LayerBrushes internal override void Initialize() { - TryOrBreak(InitializeProperties, "Failed to initialize"); + TryOrBreak(() => InitializeProperties(Layer.LayerEntity.LayerBrush?.PropertyGroup), "Failed to initialize"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs index 6fb91ca71..8aa7026e0 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; using Ninject; namespace Artemis.Core.LayerBrushes @@ -57,23 +58,20 @@ namespace Artemis.Core.LayerBrushes /// /// Creates an instance of the described brush and applies it to the layer /// - internal void CreateInstance(Layer layer) + public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity) { - if (layer == null) throw new ArgumentNullException(nameof(layer)); - if (layer.LayerBrush != null) - throw new ArtemisCoreException("Layer already has an instantiated layer brush"); + if (layer == null) + throw new ArgumentNullException(nameof(layer)); BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); brush.Layer = layer; brush.Descriptor = this; + brush.LayerBrushEntity = entity ?? new LayerBrushEntity { ProviderId = Provider.Id, BrushType = LayerBrushType.FullName }; + brush.Initialize(); brush.Update(0); - layer.LayerBrush = brush; - layer.OnLayerBrushUpdated(); - - if (layer.ShouldBeEnabled) - brush.InternalEnable(); + return brush; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs index 1f1948808..479738e15 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs @@ -77,7 +77,7 @@ namespace Artemis.Core.LayerBrushes internal override void Initialize() { - TryOrBreak(InitializeProperties, "Failed to initialize"); + TryOrBreak(() => InitializeProperties(Layer.LayerEntity.LayerBrush?.PropertyGroup), "Failed to initialize"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index ce32edfd0..24c46589a 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.LayerEffects @@ -24,16 +25,13 @@ namespace Artemis.Core.LayerEffects _profileElement = null!; _descriptor = null!; _name = null!; + LayerEffectEntity = null!; } /// - /// Gets the unique ID of this effect + /// Gets the /// - public Guid EntityId - { - get => _entityId; - internal set => SetAndNotify(ref _entityId, value); - } + public LayerEffectEntity LayerEffectEntity { get; internal set; } /// /// Gets the profile element (such as layer or folder) this effect is applied to @@ -109,8 +107,6 @@ namespace Artemis.Core.LayerEffects /// public virtual LayerPropertyGroup? BaseProperties => null; - internal string PropertyRootPath => $"LayerEffect.{EntityId}.{GetType().Name}."; - /// /// Gets a boolean indicating whether the layer effect is enabled or not /// @@ -227,5 +223,17 @@ namespace Artemis.Core.LayerEffects public override string BrokenDisplayName => Name; #endregion + + public void Save() + { + // No need to update the ID, type and provider ID. They're set once by the LayerBrushDescriptors CreateInstance and can't change + LayerEffectEntity.Name = Name; + LayerEffectEntity.Suspended = Suspended; + LayerEffectEntity.HasBeenRenamed = HasBeenRenamed; + LayerEffectEntity.Order = Order; + + BaseProperties?.ApplyToEntity(); + LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs index 3a1b03e62..8916758e7 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs @@ -36,8 +36,7 @@ namespace Artemis.Core.LayerEffects internal void InitializeProperties() { Properties = Activator.CreateInstance(); - Properties.LayerEffect = this; - Properties.Initialize(ProfileElement, PropertyRootPath, Descriptor.Provider); + Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute(){Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup); PropertiesInitialized = true; EnableLayerEffect(); diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs index f724f367a..34f501845 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs @@ -54,11 +54,28 @@ namespace Artemis.Core.LayerEffects /// /// Creates an instance of the described effect and applies it to the render element /// - internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity? entity) { - // Skip effects already on the element - if (renderElement.LayerEffects.Any(e => e.EntityId == entity.Id)) - return; + if (LayerEffectType == null) + throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); + + if (entity == null) + { + entity = new LayerEffectEntity + { + Id = Guid.NewGuid(), + Suspended = false, + Order = renderElement.LayerEffects.Count + 1, + ProviderId = Provider.Id, + EffectType = LayerEffectType.FullName + }; + } + else + { + // Skip effects already on the element + if (renderElement.LayerEffects.Any(e => e.LayerEffectEntity.Id == entity.Id)) + return; + } if (PlaceholderFor != null) { @@ -66,12 +83,9 @@ namespace Artemis.Core.LayerEffects return; } - if (LayerEffectType == null) - throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); - BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType); effect.ProfileElement = renderElement; - effect.EntityId = entity.Id; + effect.LayerEffectEntity = entity; effect.Order = entity.Order; effect.Name = entity.Name; effect.Suspended = entity.Suspended; diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs index 07f57491b..a1ec92371 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs @@ -13,7 +13,7 @@ namespace Artemis.Core.LayerEffects.Placeholder OriginalEntity = originalEntity; PlaceholderFor = placeholderFor; - EntityId = OriginalEntity.Id; + LayerEffectEntity = originalEntity; Order = OriginalEntity.Order; Name = OriginalEntity.Name; Suspended = OriginalEntity.Suspended; diff --git a/src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs new file mode 100644 index 000000000..1e99d1318 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/LayerBrushEntity.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile; + +public class LayerBrushEntity +{ + public string ProviderId { get; set; } + public string BrushType { get; set; } + + public PropertyGroupEntity PropertyGroup { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs index a575e24a9..236d1d2fb 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEffectEntity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Artemis.Storage.Entities.Profile { @@ -11,5 +12,7 @@ namespace Artemis.Storage.Entities.Profile public bool Suspended { get; set; } public bool HasBeenRenamed { get; set; } public int Order { get; set; } + + public PropertyGroupEntity PropertyGroup { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 4be0f1d02..1c8ff29ea 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -24,9 +24,12 @@ namespace Artemis.Storage.Entities.Profile public List Leds { get; set; } public List AdaptionHints { get; set; } + public PropertyGroupEntity GeneralPropertyGroup { get; set; } + public PropertyGroupEntity TransformPropertyGroup { get; set; } + public LayerBrushEntity LayerBrush { get; set; } + [BsonRef("ProfileEntity")] public ProfileEntity Profile { get; set; } - public Guid ProfileId { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs index ec5fc0344..21036efcc 100644 --- a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs @@ -1,22 +1,14 @@ using System.Collections.Generic; using Artemis.Storage.Entities.Profile.DataBindings; -namespace Artemis.Storage.Entities.Profile +namespace Artemis.Storage.Entities.Profile; + +public class PropertyEntity { - public class PropertyEntity - { - public PropertyEntity() - { - KeyframeEntities = new List(); - } + public string Identifier { get; set; } + public string Value { get; set; } + public bool KeyframesEnabled { get; set; } - public string FeatureId { get; set; } - public string Path { get; set; } - public DataBindingEntity DataBinding { get; set; } - - public string Value { get; set; } - public bool KeyframesEnabled { get; set; } - - public List KeyframeEntities { get; set; } - } + public DataBindingEntity DataBinding { get; set; } + public List KeyframeEntities { get; set; } = new(); } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs new file mode 100644 index 000000000..3f4825782 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/PropertyGroupEntity.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Artemis.Storage.Entities.Profile; + +public class PropertyGroupEntity +{ + public string Identifier { get; set; } + public List Properties { get; set; } = new(); + public List PropertyGroups { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index bdea29e96..2b4be9532 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -49,13 +49,13 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { if (LayerProperty.ProfileElement is Layer layer) { - layer.ChangeLayerBrush(SelectedDescriptor); - if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) - Execute.PostToUIThread(async () => - { - await Task.Delay(400); - await _dialogService.ShowDialogAt("LayerProperties", new Dictionary {{"layerBrush", layer.LayerBrush}}); - }); + // layer.ChangeLayerBrush(SelectedDescriptor); + // if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) + // Execute.PostToUIThread(async () => + // { + // await Task.Delay(400); + // await _dialogService.ShowDialogAt("LayerProperties", new Dictionary {{"layerBrush", layer.LayerBrush}}); + // }); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 4c506471b..41c7a1a23 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -332,19 +332,19 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties if (SelectedProfileElement == null) return; - // Remove VMs of effects no longer applied on the layer - List toRemove = Items - .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) - .ToList(); - Items.RemoveRange(toRemove); - - foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects) - { - if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) - continue; - - Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties)); - } + // // Remove VMs of effects no longer applied on the layer + // List toRemove = Items + // .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) + // .ToList(); + // Items.RemoveRange(toRemove); + // + // foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects) + // { + // if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) + // continue; + // + // Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties)); + // } SortProperties(); } @@ -355,11 +355,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties List nonEffectProperties = Items .Where(l => l.TreeGroupViewModel.GroupType != LayerEffectRoot) .ToList(); - // Order the effects - List effectProperties = Items - .Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) - .OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order) - .ToList(); + // // Order the effects + // List effectProperties = Items + // .Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) + // .OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order) + // .ToList(); // Put the non-effect properties in front for (int index = 0; index < nonEffectProperties.Count; index++) @@ -369,13 +369,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties ((BindableCollection) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index); } - // Put the effect properties after, sorted by their order - for (int index = 0; index < effectProperties.Count; index++) - { - LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; - if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) - ((BindableCollection) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); - } + // // Put the effect properties after, sorted by their order + // for (int index = 0; index < effectProperties.Count; index++) + // { + // LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; + // if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) + // ((BindableCollection) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); + // } } public async void ToggleEffectsViewModel() diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs index 84e28c8a3..ce26802e1 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertyGroupViewModel.cs @@ -87,8 +87,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties public void UpdateOrder(int order) { - if (LayerPropertyGroup.LayerEffect != null) - LayerPropertyGroup.LayerEffect.Order = order; + // if (LayerPropertyGroup.LayerEffect != null) + // LayerPropertyGroup.LayerEffect.Order = order; NotifyOfPropertyChange(nameof(IsExpanded)); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs index c05e3f26b..caaebf9bb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs @@ -59,7 +59,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models public KeyframeClipboardModel(ILayerPropertyKeyframe layerPropertyKeyframe) { - FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id; + // FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id; Path = layerPropertyKeyframe.UntypedLayerProperty.Path; KeyframeEntity = layerPropertyKeyframe.GetKeyframeEntity(); } @@ -70,15 +70,15 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models public ILayerPropertyKeyframe Paste(List properties, TimeSpan offset) { - ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path); - if (property != null) - { - KeyframeEntity.Position += offset; - ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity); - KeyframeEntity.Position -= offset; - - return keyframe; - } + // ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path); + // if (property != null) + // { + // KeyframeEntity.Position += offset; + // ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity); + // KeyframeEntity.Position -= offset; + // + // return keyframe; + // } return null; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs index 01b7417db..cf003c3aa 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs @@ -56,94 +56,94 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree ? LayerPropertyGroupViewModel.Items : null; - public void OpenBrushSettings() - { - BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush; - LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog; - if (configurationViewModel == null) - return; + // public void OpenBrushSettings() + // { + // BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush; + // LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog; + // if (configurationViewModel == null) + // return; + // + // try + // { + // // Limit to one constructor, there's no need to have more and it complicates things anyway + // ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); + // if (constructors.Length != 1) + // throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); + // + // // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure + // ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); + // ConstructorArgument argument = new(brushParameter.Name, layerBrush); + // BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); + // + // _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel); + // _windowManager.ShowDialog(_layerBrushSettingsWindowVm); + // + // // Save changes after the dialog closes + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } + // catch (Exception e) + // { + // _dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e); + // } + // } - try - { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); + // public void OpenEffectSettings() + // { + // BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect; + // LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog; + // if (configurationViewModel == null) + // return; + // + // try + // { + // // Limit to one constructor, there's no need to have more and it complicates things anyway + // ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); + // if (constructors.Length != 1) + // throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor"); + // + // ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); + // ConstructorArgument argument = new(effectParameter.Name, layerEffect); + // EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); + // + // _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel); + // _windowManager.ShowDialog(_layerEffectSettingsWindowVm); + // + // // Save changes after the dialog closes + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } + // catch (Exception e) + // { + // _dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e); + // throw; + // } + // } - // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure - ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); - ConstructorArgument argument = new(brushParameter.Name, layerBrush); - BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); - - _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel); - _windowManager.ShowDialog(_layerBrushSettingsWindowVm); - - // Save changes after the dialog closes - _profileEditorService.SaveSelectedProfileConfiguration(); - } - catch (Exception e) - { - _dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e); - } - } - - public void OpenEffectSettings() - { - BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect; - LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog; - if (configurationViewModel == null) - return; - - try - { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor"); - - ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); - ConstructorArgument argument = new(effectParameter.Name, layerEffect); - EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); - - _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel); - _windowManager.ShowDialog(_layerEffectSettingsWindowVm); - - // Save changes after the dialog closes - _profileEditorService.SaveSelectedProfileConfiguration(); - } - catch (Exception e) - { - _dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e); - throw; - } - } - - public async void RenameEffect() - { - object result = await _dialogService.ShowDialogAt( - "PropertyTreeDialogHost", - new Dictionary - { - {"subject", "effect"}, - {"currentName", LayerPropertyGroup.LayerEffect.Name} - } - ); - if (result is string newName) - { - LayerPropertyGroup.LayerEffect.Name = newName; - LayerPropertyGroup.LayerEffect.HasBeenRenamed = true; - _profileEditorService.SaveSelectedProfileConfiguration(); - } - } - - public void DeleteEffect() - { - if (LayerPropertyGroup.LayerEffect == null) - return; - - LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect); - _profileEditorService.SaveSelectedProfileConfiguration(); - } + // public async void RenameEffect() + // { + // object result = await _dialogService.ShowDialogAt( + // "PropertyTreeDialogHost", + // new Dictionary + // { + // {"subject", "effect"}, + // {"currentName", LayerPropertyGroup.LayerEffect.Name} + // } + // ); + // if (result is string newName) + // { + // LayerPropertyGroup.LayerEffect.Name = newName; + // LayerPropertyGroup.LayerEffect.HasBeenRenamed = true; + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } + // } + // + // public void DeleteEffect() + // { + // if (LayerPropertyGroup.LayerEffect == null) + // return; + // + // LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect); + // _profileEditorService.SaveSelectedProfileConfiguration(); + // } public void SuspendedToggled() { @@ -175,10 +175,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree GroupType = LayerPropertyGroupType.General; else if (LayerPropertyGroup is LayerTransformProperties) GroupType = LayerPropertyGroupType.Transform; - else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null) - GroupType = LayerPropertyGroupType.LayerBrushRoot; - else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) - GroupType = LayerPropertyGroupType.LayerEffectRoot; + // else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null) + // GroupType = LayerPropertyGroupType.LayerBrushRoot; + // else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) + // GroupType = LayerPropertyGroupType.LayerEffectRoot; else GroupType = LayerPropertyGroupType.None; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index ae3ed6dde..fb6a06c69 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -151,8 +151,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem ProfileElement.AddChild(layer, 0); // Could be null if the default brush got disabled LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); - if (brush != null) - layer.ChangeLayerBrush(brush); + // if (brush != null) + // layer.ChangeLayerBrush(brush); layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds)); _profileEditorService.SaveSelectedProfileConfiguration(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs index 9e56b678c..45181bd84 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs @@ -93,8 +93,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools folder.AddChild(newLayer); LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); - if (brush != null) - newLayer.ChangeLayerBrush(brush); + // if (brush != null) + // newLayer.ChangeLayerBrush(brush); newLayer.AddLeds(selectedLeds); ProfileEditorService.ChangeSelectedProfileElement(newLayer); diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs index 2032431a6..a93d99142 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerBrush.cs @@ -1,4 +1,5 @@ -using Artemis.Core; +using System; +using Artemis.Core; using Artemis.Core.LayerBrushes; namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; @@ -6,11 +7,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; /// /// Represents a profile editor command that can be used to change the brush of a layer. /// -public class ChangeLayerBrush : IProfileEditorCommand +public class ChangeLayerBrush : IProfileEditorCommand, IDisposable { private readonly Layer _layer; private readonly LayerBrushDescriptor _layerBrushDescriptor; - private readonly LayerBrushDescriptor? _previousDescriptor; + private readonly BaseLayerBrush? _previousBrush; + + private BaseLayerBrush? _newBrush; + private bool _executed; /// /// Creates a new instance of the class. @@ -19,7 +23,7 @@ public class ChangeLayerBrush : IProfileEditorCommand { _layer = layer; _layerBrushDescriptor = layerBrushDescriptor; - _previousDescriptor = layer.LayerBrush?.Descriptor; + _previousBrush = _layer.LayerBrush; } #region Implementation of IProfileEditorCommand @@ -30,14 +34,32 @@ public class ChangeLayerBrush : IProfileEditorCommand /// public void Execute() { - _layer.ChangeLayerBrush(_layerBrushDescriptor); + // Create the new brush + _newBrush ??= _layerBrushDescriptor.CreateInstance(_layer, null); + // Change the brush to the new brush + _layer.ChangeLayerBrush(_newBrush); + + _executed = true; } /// public void Undo() { - if (_previousDescriptor != null) - _layer.ChangeLayerBrush(_previousDescriptor); + _layer.ChangeLayerBrush(_previousBrush); + _executed = false; + } + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + if (_executed) + _previousBrush?.Dispose(); + else + _newBrush?.Dispose(); } #endregion diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index b759fe8cc..aaf57c0be 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -3,22 +3,86 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Shared.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor; + +/// +/// Provides access the the profile editor back-end logic. +/// +public interface IProfileEditorService : IArtemisSharedUIService { - public interface IProfileEditorService : IArtemisSharedUIService - { - IObservable ProfileConfiguration { get; } - IObservable ProfileElement { get; } - IObservable History { get; } - IObservable Time { get; } - IObservable PixelsPerSecond { get; } + /// + /// Gets an observable of the currently selected profile configuration. + /// + IObservable ProfileConfiguration { get; } - void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); - void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); - void ChangeTime(TimeSpan time); + /// + /// Gets an observable of the currently selected profile element. + /// + IObservable ProfileElement { get; } - void ExecuteCommand(IProfileEditorCommand command); - void SaveProfile(); - Task SaveProfileAsync(); - } + /// + /// Gets an observable of the current editor history. + /// + IObservable History { get; } + + /// + /// Gets an observable of the profile preview playback time. + /// + IObservable Time { get; } + + /// + /// Gets an observable of the profile preview playing state. + /// + IObservable Playing { get; } + + /// + /// Gets an observable of the zoom level. + /// + IObservable PixelsPerSecond { get; } + + + /// + /// Changes the selected profile by its . + /// + /// The profile configuration of the profile to select. + void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); + + /// + /// Changes the selected profile element. + /// + /// The profile element to select. + void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); + + /// + /// Changes the current profile preview playback time. + /// + /// The new time. + void ChangeTime(TimeSpan time); + + /// + /// Executes the provided command and adds it to the history. + /// + /// The command to execute. + void ExecuteCommand(IProfileEditorCommand command); + + /// + /// Saves the current profile. + /// + void SaveProfile(); + + /// + /// Asynchronously saves the current profile. + /// + /// A task representing the save action. + Task SaveProfileAsync(); + + /// + /// Resumes profile preview playback. + /// + void Play(); + + /// + /// Pauses profile preview playback. + /// + void Pause(); } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 0441f068f..ec9d5f05e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; +using Serilog; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -15,19 +16,27 @@ internal class ProfileEditorService : IProfileEditorService private readonly Dictionary _profileEditorHistories = new(); private readonly BehaviorSubject _profileElementSubject = new(null); private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero); + private readonly BehaviorSubject _playingSubject = new(false); + private readonly BehaviorSubject _suspendedEditingSubject = new(false); private readonly BehaviorSubject _pixelsPerSecondSubject = new(300); + private readonly ILogger _logger; private readonly IProfileService _profileService; + private readonly IModuleService _moduleService; private readonly IWindowService _windowService; - public ProfileEditorService(IProfileService profileService, IWindowService windowService) + public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService) { + _logger = logger; _profileService = profileService; + _moduleService = moduleService; _windowService = windowService; ProfileConfiguration = _profileConfigurationSubject.AsObservable(); ProfileElement = _profileElementSubject.AsObservable(); History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); Time = _timeSubject.AsObservable(); + Playing = _playingSubject.AsObservable(); + SuspendedEditing = _suspendedEditingSubject.AsObservable(); PixelsPerSecond = _pixelsPerSecondSubject.AsObservable(); } @@ -47,10 +56,46 @@ internal class ProfileEditorService : IProfileEditorService public IObservable ProfileElement { get; } public IObservable History { get; } public IObservable Time { get; } + public IObservable Playing { get; } + public IObservable SuspendedEditing { get; } public IObservable PixelsPerSecond { get; } public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) { + if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration)) + return; + + _logger.Verbose("ChangeCurrentProfileConfiguration {profile}", profileConfiguration); + + // Stop playing and save the current profile + Pause(); + if (_profileConfigurationSubject.Value?.Profile != null) + _profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value; + SaveProfile(); + + // No need to deactivate the profile, if needed it will be deactivated next update + if (_profileConfigurationSubject.Value != null) + _profileConfigurationSubject.Value.IsBeingEdited = false; + + // Deselect whatever profile element was active + ChangeCurrentProfileElement(null); + + // The new profile may need activation + if (profileConfiguration != null) + { + profileConfiguration.IsBeingEdited = true; + _moduleService.SetActivationOverride(profileConfiguration.Module); + _profileService.ActivateProfile(profileConfiguration); + _profileService.RenderForEditor = true; + + if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement) + ChangeCurrentProfileElement(renderProfileElement); + } + else + { + _moduleService.SetActivationOverride(null); + _profileService.RenderForEditor = false; + } _profileConfigurationSubject.OnNext(profileConfiguration); } @@ -61,6 +106,7 @@ internal class ProfileEditorService : IProfileEditorService public void ChangeTime(TimeSpan time) { + Tick(time); _timeSubject.OnNext(time); } @@ -101,4 +147,48 @@ internal class ProfileEditorService : IProfileEditorService { await Task.Run(SaveProfile); } + + /// + public void Play() + { + if (!_playingSubject.Value) + _playingSubject.OnNext(true); + } + + /// + public void Pause() + { + if (_playingSubject.Value) + _playingSubject.OnNext(false); + } + + private void Tick(TimeSpan time) + { + if (_profileConfigurationSubject.Value?.Profile == null || _suspendedEditingSubject.Value) + return; + + TickProfileElement(_profileConfigurationSubject.Value.Profile.GetRootFolder(), time); + } + + private void TickProfileElement(ProfileElement profileElement, TimeSpan time) + { + if (profileElement is not RenderProfileElement renderElement) + return; + + if (renderElement.Suspended) + { + renderElement.Disable(); + } + else + { + renderElement.Enable(); + renderElement.Timeline.Override( + time, + (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat + ); + + foreach (ProfileElement child in renderElement.Children) + TickProfileElement(child, time); + } + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index b359ef18f..302197215 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -24,8 +24,8 @@ - - + + diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Condensed.axaml similarity index 61% rename from src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml rename to src/Avalonia/Artemis.UI.Shared/Styles/Condensed.axaml index 5ce52297f..a2aad151b 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Condensed.axaml @@ -1,6 +1,7 @@  + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls"> @@ -24,6 +25,9 @@ Bluheheheheh Bluhgfdgdsheheh + + + @@ -31,19 +35,34 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index e9c0b05c0..f6b0cd9c0 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico b/src/Avalonia/Artemis.UI/Assets/Images/Logo/application.ico new file mode 100644 index 0000000000000000000000000000000000000000..bdb89dbd0af2610300c383d3f10662157b49ccfe GIT binary patch literal 113388 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 literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs b/src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs new file mode 100644 index 000000000..bd27466a6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/SKColorToColor2Converter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using FluentAvalonia.UI.Media; +using SkiaSharp; + +namespace Artemis.UI.Converters; + +/// +/// Converts into . +/// +public class SKColorToColor2Converter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is SKColor skColor) + return new Color2(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha); + return new Color2(); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Color2 color2) + return new SKColor(color2.R, color2.G, color2.B, color2.A); + return SKColor.Empty; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs b/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs new file mode 100644 index 000000000..e7707d8f1 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using SkiaSharp; + +namespace Artemis.UI.Converters; + +/// +/// +/// Converts into . +/// +public class SKColorToStringConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value?.ToString(); + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (string.IsNullOrWhiteSpace(value as string)) + return SKColor.Empty; + + return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml index 84c152c51..8452fec0f 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.BoolPropertyInputView"> - TODO + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs index 60a4fbc8a..7d98828d3 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputViewModel.cs @@ -1,13 +1,31 @@ -using Artemis.Core; +using System; +using Artemis.Core; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.PropertyInput; +using ReactiveUI; namespace Artemis.UI.DefaultTypes.PropertyInput; public class BoolPropertyInputViewModel : PropertyInputViewModel { + private BooleanOptions _selectedBooleanOption; + public BoolPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) : base(layerProperty, profileEditorService, propertyInputService) { + this.WhenAnyValue(vm => vm.InputValue).Subscribe(v => SelectedBooleanOption = v ? BooleanOptions.True : BooleanOptions.False); + this.WhenAnyValue(vm => vm.SelectedBooleanOption).Subscribe(v => InputValue = v == BooleanOptions.True); } + + public BooleanOptions SelectedBooleanOption + { + get => _selectedBooleanOption; + set => this.RaiseAndSetIfChanged(ref _selectedBooleanOption, value); + } +} + +public enum BooleanOptions +{ + True, + False } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index adab728e6..ec3832869 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -23,7 +23,7 @@ diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index 796b1e439..41059cafa 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -66,6 +66,16 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel _windowService.CreateContentDialog().WithViewModel(out LayerBrushPresetViewModel _, ("layerBrush", layer.LayerBrush)).ShowAsync()); } + #region Overrides of PropertyInputViewModel + + /// + protected override void OnInputValueChanged() + { + this.RaisePropertyChanged(nameof(SelectedDescriptor)); + } + + #endregion + private void UpdateDescriptorsIfChanged(PluginFeatureEventArgs e) { if (e.PluginFeature is not LayerBrushProvider) diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml index c8d1d0490..f04e9e77c 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/EnumPropertyInputView.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.EnumPropertyInputView"> - TODO - + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml index 7ac5dc871..233631978 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml @@ -2,7 +2,31 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:shared="clr-namespace:Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.SKColorPropertyInputView"> - TODO - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml b/src/Avalonia/Artemis.UI/MainWindow.axaml index fb57c2f6f..590aef1a0 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml @@ -5,7 +5,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.MainWindow" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis 2.0"> diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index ffbda5e3a..38b9dd0fc 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,5 +1,7 @@ using System.Collections.ObjectModel; using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor; @@ -67,6 +69,8 @@ namespace Artemis.UI.Ninject.Factories { ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty); ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush); + ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect); TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel); // TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel); diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml index 6c7eff869..325cd5362 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml @@ -7,7 +7,7 @@ xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Debugger.DebugView" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Debugger" Width="1200" Height="800"> diff --git a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml index 96cdfc9b7..7a27ae925 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Device/DevicePropertiesView.axaml @@ -6,7 +6,7 @@ xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Device.DevicePropertiesView" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Device Properties" Width="1250" Height="900"> diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 988cb4734..7bfea49dc 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -5,7 +5,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.PluginSettingsWindowView" - Icon="/Assets/Images/Logo/bow.ico" + Icon="/Assets/Images/Logo/application.ico" Title="{Binding DisplayName}" Width="800" Height="800" diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml new file mode 100644 index 000000000..e180d3ab9 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + Don't repeat the timeline + + + This setting only applies to the editor and does not affect the repeat mode during normal profile playback + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs new file mode 100644 index 000000000..ed5b99c6b --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Playback +{ + public partial class PlaybackView : ReactiveUserControl + { + public PlaybackView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs new file mode 100644 index 000000000..70c099028 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs @@ -0,0 +1,193 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Playback; + +public class PlaybackViewModel : ActivatableViewModelBase +{ + private readonly ICoreService _coreService; + private readonly IProfileEditorService _profileEditorService; + private readonly ISettingsService _settingsService; + private RenderProfileElement? _profileElement; + private ObservableAsPropertyHelper? _currentTime; + private ObservableAsPropertyHelper? _formattedCurrentTime; + private ObservableAsPropertyHelper? _playing; + private bool _repeating; + private bool _repeatTimeline; + private bool _repeatSegment; + + public PlaybackViewModel(ICoreService coreService, IProfileEditorService profileEditorService, ISettingsService settingsService) + { + _coreService = coreService; + _profileEditorService = profileEditorService; + _settingsService = settingsService; + + this.WhenActivated(d => + { + _profileEditorService.ProfileElement.Subscribe(e => _profileElement = e).DisposeWith(d); + _currentTime = _profileEditorService.Time.ToProperty(this, vm => vm.CurrentTime).DisposeWith(d); + _formattedCurrentTime = _profileEditorService.Time.Select(t => $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}").ToProperty(this, vm => vm.FormattedCurrentTime).DisposeWith(d); + _playing = _profileEditorService.Playing.ToProperty(this, vm => vm.Playing).DisposeWith(d); + + Observable.FromEventPattern(x => coreService.FrameRendering += x, x => coreService.FrameRendering -= x) + .Subscribe(e => CoreServiceOnFrameRendering(e.EventArgs)) + .DisposeWith(d); + }); + } + + public TimeSpan CurrentTime => _currentTime?.Value ?? TimeSpan.Zero; + public string? FormattedCurrentTime => _formattedCurrentTime?.Value; + public bool Playing => _playing?.Value ?? false; + + public bool Repeating + { + get => _repeating; + set => this.RaiseAndSetIfChanged(ref _repeating, value); + } + + public bool RepeatTimeline + { + get => _repeatTimeline; + set => this.RaiseAndSetIfChanged(ref _repeatTimeline, value); + } + + public bool RepeatSegment + { + get => _repeatSegment; + set => this.RaiseAndSetIfChanged(ref _repeatSegment, value); + } + + public void PlayFromStart() + { + GoToStart(); + if (!Playing) + _profileEditorService.Play(); + } + + public void TogglePlay() + { + if (!Playing) + _profileEditorService.Play(); + else + _profileEditorService.Pause(); + } + + public void GoToStart() + { + _profileEditorService.ChangeTime(TimeSpan.Zero); + } + + public void GoToEnd() + { + if (_profileElement == null) + return; + + _profileEditorService.ChangeTime(_profileElement.Timeline.EndSegmentEndPosition); + } + + public void GoToPreviousFrame() + { + if (_profileElement == null) + return; + + double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value; + double newTime = Math.Max(0, Math.Round((CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime); + _profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime)); + } + + public void GoToNextFrame() + { + if (_profileElement == null) + return; + + double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value; + double newTime = Math.Round((CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime; + newTime = Math.Min(newTime, _profileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds); + _profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime)); + } + + public void CycleRepeating() + { + if (!Repeating) + { + RepeatTimeline = true; + RepeatSegment = false; + Repeating = true; + } + else if (RepeatTimeline) + { + RepeatTimeline = false; + RepeatSegment = true; + } + else if (RepeatSegment) + { + RepeatTimeline = true; + RepeatSegment = false; + Repeating = false; + } + } + + private TimeSpan GetCurrentSegmentStart() + { + if (_profileElement == null) + return TimeSpan.Zero; + + if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition) + return TimeSpan.Zero; + if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition) + return _profileElement.Timeline.MainSegmentStartPosition; + if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition) + return _profileElement.Timeline.EndSegmentStartPosition; + + return TimeSpan.Zero; + } + + private TimeSpan GetCurrentSegmentEnd() + { + if (_profileElement == null) + return TimeSpan.Zero; + + if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition) + return _profileElement.Timeline.StartSegmentEndPosition; + if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition) + return _profileElement.Timeline.MainSegmentEndPosition; + if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition) + return _profileElement.Timeline.EndSegmentEndPosition; + + return TimeSpan.Zero; + } + + private void CoreServiceOnFrameRendering(FrameRenderingEventArgs e) + { + if (!Playing) + return; + + TimeSpan newTime = CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime)); + if (_profileElement != null) + { + if (Repeating && RepeatTimeline) + { + if (newTime > _profileElement.Timeline.Length) + newTime = TimeSpan.Zero; + } + else if (Repeating && RepeatSegment) + { + if (newTime > GetCurrentSegmentEnd()) + newTime = GetCurrentSegmentStart(); + } + else if (newTime > _profileElement.Timeline.Length) + { + newTime = _profileElement.Timeline.Length; + _profileEditorService.Pause(); + } + } + + _profileEditorService.ChangeTime(newTime); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml index 031ee5c09..de14a7b71 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml @@ -3,18 +3,45 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView"> - - - - - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs index ae979f444..69bd34970 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesView.axaml.cs @@ -1,18 +1,17 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties -{ - public partial class ProfileElementPropertiesView : ReactiveUserControl - { - public ProfileElementPropertiesView() - { - InitializeComponent(); - } +namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public class ProfileElementPropertiesView : ReactiveUserControl +{ + public ProfileElementPropertiesView() + { + InitializeComponent(); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs index 93e518660..5e396dd9b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertiesViewModel.cs @@ -6,9 +6,10 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; +using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; +using Artemis.UI.Screens.ProfileEditor.Playback; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; @@ -17,20 +18,18 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; public class ProfileElementPropertiesViewModel : ActivatableViewModelBase { + private readonly Dictionary _cachedViewModels; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; - private readonly IProfileEditorService _profileEditorService; - private ProfileElementPropertyGroupViewModel? _brushPropertyGroup; private ObservableAsPropertyHelper? _profileElement; - private readonly Dictionary> _profileElementGroups; private ObservableCollection _propertyGroupViewModels; /// - public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) + public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel) { - _profileEditorService = profileEditorService; _layerPropertyVmFactory = layerPropertyVmFactory; - PropertyGroupViewModels = new ObservableCollection(); - _profileElementGroups = new Dictionary>(); + _propertyGroupViewModels = new ObservableCollection(); + _cachedViewModels = new Dictionary(); + PlaybackViewModel = playbackViewModel; // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 this.WhenAnyValue(vm => vm.ProfileElement) @@ -47,10 +46,11 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase .Subscribe(_ => UpdateGroups()); // React to service profile element changes as long as the VM is active - this.WhenActivated(d => _profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d)); + this.WhenActivated(d => _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d)); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups()); } + public PlaybackViewModel PlaybackViewModel { get; } public RenderProfileElement? ProfileElement => _profileElement?.Value; public Layer? Layer => _profileElement?.Value as Layer; @@ -68,57 +68,51 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase return; } - if (!_profileElementGroups.TryGetValue(ProfileElement, out List? viewModels)) - { - viewModels = new List(); - _profileElementGroups[ProfileElement] = viewModels; - } - - List groups = new(); - + ObservableCollection viewModels = new(); if (Layer != null) { - // Add default layer groups - groups.Add(Layer.General); - groups.Add(Layer.Transform); - // Add brush group + // Add base VMs + viewModels.Add(GetOrCreateViewModel(Layer.General, null, null)); + viewModels.Add(GetOrCreateViewModel(Layer.Transform, null, null)); + + // Add brush VM if the brush has properties if (Layer.LayerBrush?.BaseProperties != null) - groups.Add(Layer.LayerBrush.BaseProperties); + viewModels.Add(GetOrCreateViewModel(Layer.LayerBrush.BaseProperties, Layer.LayerBrush, null)); } - // Add effect groups - foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects) - { + // Add effect VMs + foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects.OrderBy(e => e.Order)) if (layerEffect.BaseProperties != null) - groups.Add(layerEffect.BaseProperties); - } + viewModels.Add(GetOrCreateViewModel(layerEffect.BaseProperties, null, layerEffect)); - // Remove redundant VMs - viewModels.RemoveAll(vm => !groups.Contains(vm.LayerPropertyGroup)); - - // Create VMs for missing groups - foreach (LayerPropertyGroup group in groups) + // Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible + for (int index = 0; index < viewModels.Count; index++) { - if (viewModels.All(vm => vm.LayerPropertyGroup != group)) - viewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(group)); + ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel = viewModels[index]; + if (index > PropertyGroupViewModels.Count - 1) + PropertyGroupViewModels.Add(profileElementPropertyGroupViewModel); + else if (!ReferenceEquals(PropertyGroupViewModels[index], profileElementPropertyGroupViewModel)) + PropertyGroupViewModels[index] = profileElementPropertyGroupViewModel; } - // Get all non-effect properties - List nonEffectProperties = viewModels - .Where(l => l.TreeGroupViewModel.GroupType != LayerPropertyGroupType.LayerEffectRoot) - .ToList(); - // Order the effects - List effectProperties = viewModels - .Where(l => l.TreeGroupViewModel.GroupType == LayerPropertyGroupType.LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) - .OrderBy(l => l.LayerPropertyGroup.LayerEffect?.Order) - .ToList(); + while (PropertyGroupViewModels.Count > viewModels.Count) + PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1); + } - ObservableCollection propertyGroupViewModels = new(); - foreach (ProfileElementPropertyGroupViewModel viewModel in nonEffectProperties) - propertyGroupViewModels.Add(viewModel); - foreach (ProfileElementPropertyGroupViewModel viewModel in effectProperties) - propertyGroupViewModels.Add(viewModel); + private ProfileElementPropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect) + { + if (_cachedViewModels.TryGetValue(layerPropertyGroup, out ProfileElementPropertyGroupViewModel? cachedVm)) + return cachedVm; - PropertyGroupViewModels = propertyGroupViewModels; + ProfileElementPropertyGroupViewModel createdVm; + if (layerBrush != null) + createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerBrush); + else if (layerEffect != null) + createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerEffect); + else + createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup); + + _cachedViewModels[layerPropertyGroup] = createdVm; + return createdVm; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs index 081c3209c..a3b6426a3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/ProfileElementPropertyGroupViewModel.cs @@ -3,6 +3,8 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Artemis.Core; +using Artemis.Core.LayerBrushes; +using Artemis.Core.LayerEffects; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Shared; @@ -33,8 +35,23 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase PopulateChildren(); } + public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerBrush layerBrush) + : this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService) + { + LayerBrush = layerBrush; + } + + public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerEffect layerEffect) + : this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService) + { + LayerEffect = layerEffect; + } + public ObservableCollection Children { get; } public LayerPropertyGroup LayerPropertyGroup { get; } + public BaseLayerBrush? LayerBrush { get; } + public BaseLayerEffect? LayerEffect { get; } + public TreeGroupViewModel TreeGroupViewModel { get; } public bool IsVisible diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs index 16fcc9eea..15e9f84a3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/ITreePropertyViewModel.cs @@ -1,9 +1,11 @@ -using ReactiveUI; +using Artemis.Core; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; public interface ITreePropertyViewModel : IReactiveObject { + ILayerProperty BaseLayerProperty { get; } bool HasDataBinding { get; } double GetDepth(); } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml index 0ccb5b649..5f693289b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Tree/TreeGroupView.axaml @@ -70,24 +70,24 @@ Brush -  + IsVisible="{Binding LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> Extra options available! @@ -108,31 +108,31 @@ - + Effect - + IsVisible="{Binding !LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> + IsVisible="{Binding LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> @@ -160,7 +160,7 @@ Height="24" VerticalAlignment="Center" Command="{Binding OpenEffectSettings}" - IsVisible="{Binding LayerPropertyGroup.LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> + IsVisible="{Binding LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> + + - - - - + + + + - - + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml.cs new file mode 100644 index 000000000..dc16daa37 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml.cs @@ -0,0 +1,30 @@ +using System; +using System.Reactive.Linq; +using Artemis.Core; +using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree; + +public class TreePropertyView : ReactiveUserControl +{ + public TreePropertyView() + { + this.WhenActivated(d => + { + if (ViewModel != null) + Observable.FromEventPattern(e => ViewModel.BaseLayerProperty.CurrentValueSet += e, e => ViewModel.BaseLayerProperty.CurrentValueSet -= e) + .Subscribe(_ => this.BringIntoView()) + .DisposeWith(d); + }); + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs new file mode 100644 index 000000000..ada3ad296 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs @@ -0,0 +1,61 @@ +using System; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Artemis.UI.Shared.Services.PropertyInput; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree; + +internal class TreePropertyViewModel : ActivatableViewModelBase, ITreePropertyViewModel +{ + private readonly IProfileEditorService _profileEditorService; + + public TreePropertyViewModel(LayerProperty layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService, + IPropertyInputService propertyInputService) + { + _profileEditorService = profileEditorService; + + LayerProperty = layerProperty; + PropertyViewModel = propertyViewModel; + PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty); + + this.WhenActivated(d => this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d)); + } + + public LayerProperty LayerProperty { get; } + public PropertyViewModel PropertyViewModel { get; } + public PropertyInputViewModel? PropertyInputViewModel { get; } + + public bool KeyframesEnabled + { + get => LayerProperty.KeyframesEnabled; + set => UpdateKeyframesEnabled(value); + } + + private void UpdateKeyframesEnabled(bool value) + { + if (value == LayerProperty.KeyframesEnabled) + return; + + _profileEditorService.ExecuteCommand(new ToggleLayerPropertyKeyframes(LayerProperty, value)); + } + + public ILayerProperty BaseLayerProperty => LayerProperty; + public bool HasDataBinding => LayerProperty.HasDataBinding; + + public double GetDepth() + { + int depth = 0; + LayerPropertyGroup? current = LayerProperty.LayerPropertyGroup; + while (current != null) + { + depth++; + current = current.Parent; + } + + return depth; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml new file mode 100644 index 000000000..12bd2cd1e --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml.cs similarity index 87% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml.cs index 000f65596..42011416c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using Avalonia; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; +namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows; public class BrushConfigurationWindowView : ReactiveCoreWindow { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowViewModel.cs similarity index 89% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowViewModel.cs index 51d8c380a..f8a7a8383 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/BrushConfigurationWindowViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowViewModel.cs @@ -1,10 +1,9 @@ using System; using Artemis.UI.Shared; using Artemis.UI.Shared.LayerBrushes; -using Artemis.UI.Shared.LayerEffects; using Avalonia.Threading; -namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; +namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows; public class BrushConfigurationWindowViewModel : DialogViewModelBase { @@ -12,7 +11,7 @@ public class BrushConfigurationWindowViewModel : DialogViewModelBase { ConfigurationViewModel = configurationViewModel; Configuration = configuration; - + ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml new file mode 100644 index 000000000..3c11ca511 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml.cs similarity index 87% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml.cs index 2e03a2339..8f1683152 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using Avalonia; using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; +namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows; public class EffectConfigurationWindowView : ReactiveCoreWindow { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowViewModel.cs similarity index 92% rename from src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs rename to src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowViewModel.cs index f9289f450..979cab5c1 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileElementProperties/Windows/EffectConfigurationWindowViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowViewModel.cs @@ -3,7 +3,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.LayerEffects; using Avalonia.Threading; -namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; +namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows; public class EffectConfigurationWindowViewModel : DialogViewModelBase { @@ -22,7 +22,7 @@ public class EffectConfigurationWindowViewModel : DialogViewModelBase { return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult(); } - + private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e) { if (CanClose()) diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index db6236efc..f0a87752a 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -67,7 +67,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 8d52bfbfd..e75dfb043 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -2,8 +2,8 @@ using System.Reactive.Disposables; using Artemis.Core; using Artemis.UI.Screens.ProfileEditor.MenuBar; -using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; using Artemis.UI.Screens.ProfileEditor.ProfileTree; +using Artemis.UI.Screens.ProfileEditor.Properties; using Artemis.UI.Screens.ProfileEditor.StatusBar; using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Shared.Services.ProfileEditor; @@ -25,13 +25,13 @@ namespace Artemis.UI.Screens.ProfileEditor ProfileTreeViewModel profileTreeViewModel, ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, MenuBarViewModel menuBarViewModel, - ProfileElementPropertiesViewModel profileElementPropertiesViewModel, + PropertiesViewModel propertiesViewModel, StatusBarViewModel statusBarViewModel) : base(hostScreen, "profile-editor") { VisualEditorViewModel = visualEditorViewModel; ProfileTreeViewModel = profileTreeViewModel; - ProfileElementPropertiesViewModel = profileElementPropertiesViewModel; + PropertiesViewModel = propertiesViewModel; StatusBarViewModel = statusBarViewModel; if (OperatingSystem.IsWindows()) @@ -46,7 +46,7 @@ namespace Artemis.UI.Screens.ProfileEditor public VisualEditorViewModel VisualEditorViewModel { get; } public ProfileTreeViewModel ProfileTreeViewModel { get; } public MenuBarViewModel? MenuBarViewModel { get; } - public ProfileElementPropertiesViewModel ProfileElementPropertiesViewModel { get; } + public PropertiesViewModel PropertiesViewModel { get; } public StatusBarViewModel StatusBarViewModel { get; } public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; From 6f269af8d4582efb1ae54050d2d38ae04b769f8a Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Jan 2022 00:24:48 +0100 Subject: [PATCH 118/270] Profile editor service - Make keyframe selection an editor concern --- .../Profile/LayerProperties/ILayerProperty.cs | 7 + .../Profile/LayerProperties/LayerProperty.cs | 3 + .../Artemis.UI.Linux/packages.lock.json | 6 +- .../Artemis.UI.MacOS/packages.lock.json | 6 +- .../Artemis.UI.Shared.csproj | 1 + .../Controls/SelectionRectangle.cs | 465 +++++++++--------- .../ProfileEditor/IProfileEditorService.cs | 24 +- .../ProfileEditor/ProfileEditorService.cs | 58 +++ .../Artemis.UI.Shared/packages.lock.json | 17 +- .../Artemis.UI.Windows/packages.lock.json | 6 +- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 1 + .../Timeline/ITimelineKeyframeViewModel.cs | 6 +- .../Timeline/TimelineKeyframeView.axaml | 5 +- .../Timeline/TimelineKeyframeView.axaml.cs | 29 ++ .../Timeline/TimelineKeyframeViewModel.cs | 32 +- .../Properties/Timeline/TimelineView.axaml | 4 +- .../Properties/Timeline/TimelineView.axaml.cs | 32 +- .../Properties/Timeline/TimelineViewModel.cs | 65 +-- src/Avalonia/Artemis.UI/packages.lock.json | 18 +- 19 files changed, 433 insertions(+), 352 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 9a4fd8333..ddb54e1ee 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -52,6 +54,11 @@ namespace Artemis.Core /// string Path { get; } + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + ReadOnlyCollection UntypedKeyframes { get; } + /// /// Gets the type of the property /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 9cfb5769d..c2f46fad2 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -258,6 +258,9 @@ namespace Artemis.Core /// public ReadOnlyCollection> Keyframes { get; } + /// + public ReadOnlyCollection UntypedKeyframes => new(Keyframes.Cast().ToList()); + /// /// Gets the current keyframe in the timeline according to the current progress /// diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 3bc042d07..684d112e1 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -201,8 +201,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", "dependencies": { "System.Reactive": "5.0.0" } @@ -1752,6 +1752,7 @@ "Avalonia.Diagnostics": "0.10.11", "Avalonia.ReactiveUI": "0.10.11", "Avalonia.Svg.Skia": "0.10.11", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", @@ -1774,6 +1775,7 @@ "Avalonia.Xaml.Behaviors": "0.10.11.5", "Avalonia.Xaml.Interactions": "0.10.11.5", "Avalonia.Xaml.Interactivity": "0.10.11.5", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 3bc042d07..684d112e1 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -201,8 +201,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", "dependencies": { "System.Reactive": "5.0.0" } @@ -1752,6 +1752,7 @@ "Avalonia.Diagnostics": "0.10.11", "Avalonia.ReactiveUI": "0.10.11", "Avalonia.Svg.Skia": "0.10.11", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", @@ -1774,6 +1775,7 @@ "Avalonia.Xaml.Behaviors": "0.10.11.5", "Avalonia.Xaml.Interactions": "0.10.11.5", "Avalonia.Xaml.Interactivity": "0.10.11.5", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 1cd6e870b..51947d759 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 3cd56734a..3866dcfb1 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -6,233 +6,250 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; -namespace Artemis.UI.Shared.Controls +namespace Artemis.UI.Shared.Controls; + +/// +/// Visualizes an with optional per-LED colors +/// +public class SelectionRectangle : Control { /// - /// Visualizes an with optional per-LED colors + /// Defines the property. /// - public class SelectionRectangle : Control + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderBrushProperty = + AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderThicknessProperty = + AvaloniaProperty.Register(nameof(BorderThickness), 1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BorderRadiusProperty = + AvaloniaProperty.Register(nameof(BorderRadius)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty InputElementProperty = + AvaloniaProperty.Register(nameof(InputElement), notifying: OnInputElementChanged); + + /// + /// Defines the read-only property. + /// + public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect(nameof(IsSelecting), o => o.IsSelecting); + + private Rect? _absoluteRect; + private Point _absoluteStartPosition; + + private Rect? _displayRect; + private bool _isSelecting; + private IControl? _oldInputElement; + private Point _startPosition; + + /// + public SelectionRectangle() { - /// - /// Defines the property. - /// - public static readonly StyledProperty BackgroundProperty = - AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty BorderBrushProperty = - AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty BorderThicknessProperty = - AvaloniaProperty.Register(nameof(BorderThickness), 1); - - /// - /// Defines the property. - /// - public static readonly StyledProperty BorderRadiusProperty = - AvaloniaProperty.Register(nameof(BorderRadius), 0); - - /// - /// Defines the property. - /// - public static readonly StyledProperty InputElementProperty = - AvaloniaProperty.Register(nameof(InputElement), notifying: OnInputElementChanged); - - private Rect? _displayRect; - private Rect? _absoluteRect; - private IControl? _oldInputElement; - private Point _startPosition; - private Point _absoluteStartPosition; - - /// - public SelectionRectangle() - { - AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); - IsHitTestVisible = false; - } - - /// - /// Gets or sets a brush used to paint the control's background. - /// - public IBrush Background - { - get => GetValue(BackgroundProperty); - set => SetValue(BackgroundProperty, value); - } - - /// - /// Gets or sets a brush used to paint the control's border - /// - public IBrush BorderBrush - { - get => GetValue(BorderBrushProperty); - set => SetValue(BorderBrushProperty, value); - } - - /// - /// Gets or sets the width of the control's border - /// - public double BorderThickness - { - get => GetValue(BorderThicknessProperty); - set => SetValue(BorderThicknessProperty, value); - } - - /// - /// Gets or sets the radius of the control's border - /// - public double BorderRadius - { - get => GetValue(BorderRadiusProperty); - set => SetValue(BorderRadiusProperty, value); - } - - /// - /// Gets or sets the element that captures input for the selection rectangle. - /// - public IControl? InputElement - { - get => GetValue(InputElementProperty); - set => SetValue(InputElementProperty, value); - } - - /// - /// Occurs when the selection rect is being updated, indicating the user is dragging. - /// - public event EventHandler? SelectionUpdated; - - /// - /// Occurs when the selection has finished, indicating the user stopped dragging. - /// - public event EventHandler? SelectionFinished; - - /// - /// Invokes the event - /// - /// - protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e) - { - SelectionUpdated?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - /// - protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e) - { - SelectionFinished?.Invoke(this, e); - } - - private static void OnInputElementChanged(IAvaloniaObject sender, bool before) - { - ((SelectionRectangle) sender).SubscribeToInputElement(); - } - - private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - e.Pointer.Capture(this); - - _startPosition = e.GetPosition(Parent); - _absoluteStartPosition = e.GetPosition(VisualRoot); - _displayRect = null; - } - - private void ParentOnPointerMoved(object? sender, PointerEventArgs e) - { - if (!ReferenceEquals(e.Pointer.Captured, this)) - return; - - Point currentPosition = e.GetPosition(Parent); - Point absoluteCurrentPosition = e.GetPosition(VisualRoot); - - _displayRect = new Rect( - new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), - new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) - ); - _absoluteRect = new Rect( - new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)), - new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)) - ); - - OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); - InvalidateVisual(); - } - - private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (!ReferenceEquals(e.Pointer.Captured, this)) - return; - - e.Pointer.Capture(null); - - if (_displayRect != null && _absoluteRect != null) - { - OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); - e.Handled = true; - } - - _displayRect = null; - InvalidateVisual(); - } - - private void SubscribeToInputElement() - { - if (_oldInputElement != null) - { - _oldInputElement.PointerPressed -= ParentOnPointerPressed; - _oldInputElement.PointerMoved -= ParentOnPointerMoved; - _oldInputElement.PointerReleased -= ParentOnPointerReleased; - } - - _oldInputElement = InputElement; - - if (InputElement != null) - { - InputElement.PointerPressed += ParentOnPointerPressed; - InputElement.PointerMoved += ParentOnPointerMoved; - InputElement.PointerReleased += ParentOnPointerReleased; - } - } - - #region Overrides of Visual - - /// - public override void Render(DrawingContext drawingContext) - { - if (_displayRect != null) - drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius); - } - - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - SubscribeToInputElement(); - base.OnAttachedToVisualTree(e); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - if (_oldInputElement != null) - { - _oldInputElement.PointerPressed -= ParentOnPointerPressed; - _oldInputElement.PointerMoved -= ParentOnPointerMoved; - _oldInputElement.PointerReleased -= ParentOnPointerReleased; - _oldInputElement = null; - } - - base.OnDetachedFromVisualTree(e); - } - - #endregion + AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); + IsHitTestVisible = false; } + + /// + /// Gets or sets a brush used to paint the control's background. + /// + public IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + /// + /// Gets or sets a brush used to paint the control's border + /// + public IBrush BorderBrush + { + get => GetValue(BorderBrushProperty); + set => SetValue(BorderBrushProperty, value); + } + + /// + /// Gets or sets the width of the control's border + /// + public double BorderThickness + { + get => GetValue(BorderThicknessProperty); + set => SetValue(BorderThicknessProperty, value); + } + + /// + /// Gets or sets the radius of the control's border + /// + public double BorderRadius + { + get => GetValue(BorderRadiusProperty); + set => SetValue(BorderRadiusProperty, value); + } + + /// + /// Gets or sets the element that captures input for the selection rectangle. + /// + public IControl? InputElement + { + get => GetValue(InputElementProperty); + set => SetValue(InputElementProperty, value); + } + + /// + /// Gets a boolean indicating whether the selection rectangle is currently performing a selection. + /// + public bool IsSelecting + { + get => _isSelecting; + private set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value); + } + + /// + /// Occurs when the selection rect is being updated, indicating the user is dragging. + /// + public event EventHandler? SelectionUpdated; + + /// + /// Occurs when the selection has finished, indicating the user stopped dragging. + /// + public event EventHandler? SelectionFinished; + + /// + /// Invokes the event + /// + /// + protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e) + { + SelectionUpdated?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + /// + protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e) + { + SelectionFinished?.Invoke(this, e); + } + + private static void OnInputElementChanged(IAvaloniaObject sender, bool before) + { + ((SelectionRectangle) sender).SubscribeToInputElement(); + } + + private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Pointer.Capture(this); + + _startPosition = e.GetPosition(Parent); + _absoluteStartPosition = e.GetPosition(VisualRoot); + _displayRect = null; + } + + private void ParentOnPointerMoved(object? sender, PointerEventArgs e) + { + if (!ReferenceEquals(e.Pointer.Captured, this)) + return; + + Point currentPosition = e.GetPosition(Parent); + Point absoluteCurrentPosition = e.GetPosition(VisualRoot); + + _displayRect = new Rect( + new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), + new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) + ); + _absoluteRect = new Rect( + new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)), + new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)) + ); + + OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); + InvalidateVisual(); + IsSelecting = true; + } + + private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!ReferenceEquals(e.Pointer.Captured, this)) + return; + + e.Pointer.Capture(null); + + if (_displayRect != null && _absoluteRect != null) + { + OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); + e.Handled = true; + } + + _displayRect = null; + InvalidateVisual(); + IsSelecting = false; + } + + private void SubscribeToInputElement() + { + if (_oldInputElement != null) + { + _oldInputElement.PointerPressed -= ParentOnPointerPressed; + _oldInputElement.PointerMoved -= ParentOnPointerMoved; + _oldInputElement.PointerReleased -= ParentOnPointerReleased; + } + + _oldInputElement = InputElement; + + if (InputElement != null) + { + InputElement.PointerPressed += ParentOnPointerPressed; + InputElement.PointerMoved += ParentOnPointerMoved; + InputElement.PointerReleased += ParentOnPointerReleased; + } + } + + #region Overrides of Visual + + /// + public override void Render(DrawingContext drawingContext) + { + if (_displayRect != null) + drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness), _displayRect.Value, BorderRadius, BorderRadius); + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + SubscribeToInputElement(); + base.OnAttachedToVisualTree(e); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + if (_oldInputElement != null) + { + _oldInputElement.PointerPressed -= ParentOnPointerPressed; + _oldInputElement.PointerMoved -= ParentOnPointerMoved; + _oldInputElement.PointerReleased -= ParentOnPointerReleased; + _oldInputElement = null; + } + + base.OnDetachedFromVisualTree(e); + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 40cb889a5..9ce0edf0b 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Shared.Services.Interfaces; +using DynamicData; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -40,7 +41,13 @@ public interface IProfileEditorService : IArtemisSharedUIService /// Gets an observable of the zoom level. /// IObservable PixelsPerSecond { get; } - + + /// + /// Connect to the observable list of keyframes and observe any changes starting with the list's initial items. + /// + /// An observable which emits the change set. + IObservable> ConnectToKeyframes(); + /// /// Changes the selected profile by its . /// @@ -65,6 +72,21 @@ public interface IProfileEditorService : IArtemisSharedUIService /// The new pixels per second. void ChangePixelsPerSecond(int pixelsPerSecond); + /// + /// Selects the provided keyframe. + /// + /// The keyframe to select. + /// If expands the current selection; otherwise replaces it with only the provided . + /// If toggles the selection and only for the provided . + void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle); + + /// + /// Selects the provided keyframes. + /// + /// The keyframes to select. + /// If expands the current selection; otherwise replaces it with only the provided . + void SelectKeyframes(IEnumerable keyframes, bool expand); + /// /// Snaps the given time to the closest relevant element in the timeline, this can be the cursor, a keyframe or a /// segment end. diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 54de02885..339cba6b1 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -7,6 +8,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; +using DynamicData; using Serilog; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -20,6 +22,8 @@ internal class ProfileEditorService : IProfileEditorService private readonly BehaviorSubject _playingSubject = new(false); private readonly BehaviorSubject _suspendedEditingSubject = new(false); private readonly BehaviorSubject _pixelsPerSecondSubject = new(120); + private readonly SourceList _selectedKeyframes = new(); + private readonly ILogger _logger; private readonly IProfileService _profileService; private readonly IModuleService _moduleService; @@ -60,6 +64,7 @@ internal class ProfileEditorService : IProfileEditorService public IObservable Playing { get; } public IObservable SuspendedEditing { get; } public IObservable PixelsPerSecond { get; } + public IObservable> ConnectToKeyframes() => _selectedKeyframes.Connect(); public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) { @@ -97,11 +102,13 @@ internal class ProfileEditorService : IProfileEditorService _moduleService.SetActivationOverride(null); _profileService.RenderForEditor = false; } + _profileConfigurationSubject.OnNext(profileConfiguration); } public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement) { + _selectedKeyframes.Clear(); _profileElementSubject.OnNext(renderProfileElement); } @@ -111,6 +118,57 @@ internal class ProfileEditorService : IProfileEditorService _timeSubject.OnNext(time); } + public void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle) + { + if (keyframe == null) + { + if (!expand) + _selectedKeyframes.Clear(); + return; + } + + if (toggle) + { + // Toggle only the clicked keyframe, leave others alone + if (_selectedKeyframes.Items.Contains(keyframe)) + _selectedKeyframes.Remove(keyframe); + else + _selectedKeyframes.Add(keyframe); + } + else + { + if (expand) + { + _selectedKeyframes.Add(keyframe); + } + else + { + _selectedKeyframes.Edit(l => + { + l.Clear(); + l.Add(keyframe); + }); + } + } + } + + public void SelectKeyframes(IEnumerable keyframes, bool expand) + { + if (expand) + { + List toAdd = keyframes.Except(_selectedKeyframes.Items).ToList(); + _selectedKeyframes.AddRange(toAdd); + } + else + { + _selectedKeyframes.Edit(l => + { + l.Clear(); + l.AddRange(keyframes); + }); + } + } + public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List? snapTimes = null) { RenderProfileElement? profileElement = _profileElementSubject.Value; diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index ee416f1bd..7bd70aa2e 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -70,6 +70,15 @@ "Avalonia": "0.10.11" } }, + "DynamicData": { + "type": "Direct", + "requested": "[7.4.9, )", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, "FluentAvaloniaUI": { "type": "Direct", "requested": "[1.1.8, )", @@ -240,14 +249,6 @@ "System.Xml.XmlDocument": "4.3.0" } }, - "DynamicData": { - "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", - "dependencies": { - "System.Reactive": "5.0.0" - } - }, "EmbedIO": { "type": "Transitive", "resolved": "3.4.3", diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index 213018047..b5bc4d22b 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -217,8 +217,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.3", - "contentHash": "7eGyREbtzyaRutMa+iToi2e41JboEVK9c1ZBcTvJOfEoTRIZX3hChIsxIvV0ErzMXtGHAIS2O0I8jLDUIds5wg==", + "resolved": "7.4.9", + "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", "dependencies": { "System.Reactive": "5.0.0" } @@ -1768,6 +1768,7 @@ "Avalonia.Diagnostics": "0.10.11", "Avalonia.ReactiveUI": "0.10.11", "Avalonia.Svg.Skia": "0.10.11", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", @@ -1790,6 +1791,7 @@ "Avalonia.Xaml.Behaviors": "0.10.11.5", "Avalonia.Xaml.Interactions": "0.10.11.5", "Avalonia.Xaml.Interactivity": "0.10.11.5", + "DynamicData": "7.4.9", "FluentAvaloniaUI": "1.1.8", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index a3a0e83e1..21e8cea0e 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs index 4dfaa86d2..d91a2d9a5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs @@ -5,12 +5,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; public interface ITimelineKeyframeViewModel { - bool IsSelected { get; set; } + bool IsSelected { get; } TimeSpan Position { get; } ILayerPropertyKeyframe Keyframe { get; } #region Movement + void Select(bool expand, bool toggle); + // void StartMovement(); + // void FinishMovement(); + void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source); void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source); void UpdatePosition(TimeSpan position); diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml index fb1782854..6bcd67db5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml @@ -13,7 +13,10 @@ Height="10" Margin="-5,0,0,0" ToolTip.Tip="{Binding Timestamp}" - Classes.selected="{Binding IsSelected}"> + Classes.selected="{Binding IsSelected}" + PointerPressed="InputElement_OnPointerPressed" + PointerReleased="InputElement_OnPointerReleased" + PointerMoved="InputElement_OnPointerMoved"> + - - - - - - - - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs index 42b7d25c7..c71564082 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -43,4 +44,9 @@ public class TimelineKeyframeView : ReactiveUserControl : ActivatableViewModelBase, ITimelineK { _pixelsPerSecond = p; profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d); - System.Reactive.Disposables.Disposable.Create(() => - { - foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels) - timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected; - }).DisposeWith(d); }).DisposeWith(d); _isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d); @@ -142,31 +139,45 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)) .Cast() - .Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction))); - - foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels) - timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected; + .Select(e => new TimelineEasingViewModel(e, Keyframe))); } - public void ClearEasingViewModels() + public void SelectEasingFunction(Easings.Functions easingFunction) { - EasingViewModels.Clear(); + _profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction)); } - private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e) + #endregion +} + +public class ChangeKeyframeEasing : IProfileEditorCommand +{ + private readonly ILayerPropertyKeyframe _keyframe; + private readonly Easings.Functions _easingFunction; + private readonly Easings.Functions _originalEasingFunction; + + public ChangeKeyframeEasing(ILayerPropertyKeyframe keyframe, Easings.Functions easingFunction) { - if (sender is TimelineEasingViewModel timelineEasingViewModel) - SelectEasingMode(timelineEasingViewModel); + _keyframe = keyframe; + _easingFunction = easingFunction; + _originalEasingFunction = keyframe.EasingFunction; } - public void SelectEasingMode(TimelineEasingViewModel easingViewModel) - { - throw new NotImplementedException(); + #region Implementation of IProfileEditorCommand - LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction; - // Set every selection to false except on the VM that made the change - foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel)) - propertyTrackEasingViewModel.IsEasingModeSelected = false; + /// + public string DisplayName => "Change easing to " + _easingFunction.Humanize(LetterCasing.LowerCase); + + /// + public void Execute() + { + _keyframe.EasingFunction = _easingFunction; + } + + /// + public void Undo() + { + _keyframe.EasingFunction = _originalEasingFunction; } #endregion From 913117ad0a42b68e698fd68e0a9edfeaf6e27811 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 22 Jan 2022 01:19:10 +0100 Subject: [PATCH 120/270] Profile editor - Implemented keyframe selection, movement, deletion Profile editor - Added command scoping --- .../Profile/LayerProperties/LayerProperty.cs | 5 +- .../LayerProperties/LayerPropertyKeyFrame.cs | 1 + .../CompositeCommand.cs} | 33 ++++- .../ProfileEditor/Commands/DeleteKeyframe.cs | 38 +++++ .../ProfileEditor/Commands/MoveKeyframe.cs | 10 ++ .../ProfileEditor/IProfileEditorService.cs | 35 ++++- .../ProfileEditorCommandScope.cs | 44 ++++++ .../ProfileEditor/ProfileEditorHistory.cs | 139 +++++++++--------- .../ProfileEditor/ProfileEditorService.cs | 58 ++++++++ .../Panels/Properties/PropertiesView.axaml.cs | 2 +- .../Timeline/ITimelineKeyframeViewModel.cs | 17 ++- .../Timeline/TimelineKeyframeView.axaml | 11 +- .../Timeline/TimelineKeyframeView.axaml.cs | 37 ++++- .../Timeline/TimelineKeyframeViewModel.cs | 129 +++++++--------- .../Properties/Timeline/TimelineViewModel.cs | 128 +++++++++++++++- 15 files changed, 515 insertions(+), 172 deletions(-) rename src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/{CompositeProfileEditorCommand.cs => Commands/CompositeCommand.cs} (51%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index c2f46fad2..981805482 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -213,7 +213,7 @@ namespace Artemis.Core SetCurrentValue(CoreJson.DeserializeObject(json)!, null); } - private void ReapplyUpdate() + internal void ReapplyUpdate() { // Create a timeline with the same position but a delta of zero Timeline temporaryTimeline = new(); @@ -291,6 +291,7 @@ namespace Artemis.Core KeyframesEnabled = true; SortKeyframes(); + ReapplyUpdate(); OnKeyframeAdded(); } @@ -323,7 +324,9 @@ namespace Artemis.Core return; _keyframes.Remove(keyframe); + SortKeyframes(); + ReapplyUpdate(); OnKeyframeRemoved(); } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index 299c0638e..1f26a1876 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -57,6 +57,7 @@ namespace Artemis.Core { SetAndNotify(ref _position, value); LayerProperty.SortKeyframes(); + LayerProperty.ReapplyUpdate(); } } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/CompositeCommand.cs similarity index 51% rename from src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs rename to src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/CompositeCommand.cs index e2a2eb4e4..9dea007c7 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/CompositeProfileEditorCommand.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/CompositeCommand.cs @@ -2,21 +2,22 @@ using System.Collections.Generic; using System.Linq; -namespace Artemis.UI.Shared.Services.ProfileEditor; +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; /// /// Represents a profile editor command that can be used to combine multiple commands into one. /// -public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable +public class CompositeCommand : IProfileEditorCommand, IDisposable { + private bool _ignoreNextExecute; private readonly List _commands; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The commands to execute. /// The display name of the composite command. - public CompositeProfileEditorCommand(IEnumerable commands, string displayName) + public CompositeCommand(IEnumerable commands, string displayName) { if (commands == null) throw new ArgumentNullException(nameof(commands)); @@ -24,6 +25,22 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); } + /// + /// Creates a new instance of the class. + /// + /// The commands to execute. + /// The display name of the composite command. + /// Whether or not to ignore the first execute because commands are already executed + internal CompositeCommand(IEnumerable commands, string displayName, bool ignoreFirstExecute) + { + if (commands == null) + throw new ArgumentNullException(nameof(commands)); + + _ignoreNextExecute = ignoreFirstExecute; + _commands = commands.ToList(); + DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); + } + /// public void Dispose() { @@ -40,6 +57,12 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable /// public void Execute() { + if (_ignoreNextExecute) + { + _ignoreNextExecute = false; + return; + } + foreach (IProfileEditorCommand profileEditorCommand in _commands) profileEditorCommand.Execute(); } @@ -48,7 +71,7 @@ public class CompositeProfileEditorCommand : IProfileEditorCommand, IDisposable public void Undo() { // Undo in reverse by iterating from the back - for (int index = _commands.Count; index >= 0; index--) + for (int index = _commands.Count - 1; index >= 0; index--) _commands[index].Undo(); } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs new file mode 100644 index 000000000..4f6b552bb --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/DeleteKeyframe.cs @@ -0,0 +1,38 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to delete a keyframe. +/// +public class DeleteKeyframe : IProfileEditorCommand +{ + private readonly LayerPropertyKeyframe _keyframe; + + /// + /// Creates a new instance of the class. + /// + public DeleteKeyframe(LayerPropertyKeyframe keyframe) + { + _keyframe = keyframe; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Delete keyframe"; + + /// + public void Execute() + { + _keyframe.LayerProperty.RemoveKeyframe(_keyframe); + } + + /// + public void Undo() + { + _keyframe.LayerProperty.AddKeyframe(_keyframe); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs index 790cf229d..26faf2bfc 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveKeyframe.cs @@ -22,6 +22,16 @@ public class MoveKeyframe : IProfileEditorCommand _originalPosition = keyframe.Position; } + /// + /// Creates a new instance of the class. + /// + public MoveKeyframe(ILayerPropertyKeyframe keyframe, TimeSpan position, TimeSpan originalPosition) + { + _keyframe = keyframe; + _position = position; + _originalPosition = originalPosition; + } + #region Implementation of IProfileEditorCommand /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 9ce0edf0b..7be4f2843 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -43,7 +43,7 @@ public interface IProfileEditorService : IArtemisSharedUIService IObservable PixelsPerSecond { get; } /// - /// Connect to the observable list of keyframes and observe any changes starting with the list's initial items. + /// Connect to the observable list of keyframes and observe any changes starting with the list's initial items. /// /// An observable which emits the change set. IObservable> ConnectToKeyframes(); @@ -73,18 +73,27 @@ public interface IProfileEditorService : IArtemisSharedUIService void ChangePixelsPerSecond(int pixelsPerSecond); /// - /// Selects the provided keyframe. + /// Selects the provided keyframe. /// /// The keyframe to select. - /// If expands the current selection; otherwise replaces it with only the provided . - /// If toggles the selection and only for the provided . + /// + /// If expands the current selection; otherwise replaces it with only the + /// provided . + /// + /// + /// If toggles the selection and only for the provided + /// . + /// void SelectKeyframe(ILayerPropertyKeyframe? keyframe, bool expand, bool toggle); /// - /// Selects the provided keyframes. + /// Selects the provided keyframes. /// /// The keyframes to select. - /// If expands the current selection; otherwise replaces it with only the provided . + /// + /// If expands the current selection; otherwise replaces it with only the + /// provided . + /// void SelectKeyframes(IEnumerable keyframes, bool expand); /// @@ -99,12 +108,26 @@ public interface IProfileEditorService : IArtemisSharedUIService /// The snapped time. TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List? snapTimes = null); + /// + /// Rounds the given time to something appropriate for the current zoom level. + /// + /// The time to round + /// The rounded time. + TimeSpan RoundTime(TimeSpan time); + /// /// Executes the provided command and adds it to the history. /// /// The command to execute. void ExecuteCommand(IProfileEditorCommand command); + /// + /// Creates a new command scope which can be used to group undo/redo actions of multiple commands. + /// + /// The name of the command scope. + /// The command scope that will group any commands until disposed. + ProfileEditorCommandScope CreateCommandScope(string name); + /// /// Saves the current profile. /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs new file mode 100644 index 000000000..aa760b121 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorCommandScope.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Artemis.UI.Shared.Services.ProfileEditor; + +/// +/// Represents a scope in which editor commands are executed until disposed. +/// +public class ProfileEditorCommandScope : IDisposable +{ + private readonly List _commands; + + private readonly ProfileEditorService _profileEditorService; + + internal ProfileEditorCommandScope(ProfileEditorService profileEditorService, string name) + { + Name = name; + _profileEditorService = profileEditorService; + _commands = new List(); + } + + /// + /// Gets the name of the scope. + /// + public string Name { get; } + + /// + /// Gets a read only collection of commands in the scope. + /// + public ReadOnlyCollection ProfileEditorCommands => new(_commands); + + internal void AddCommand(IProfileEditorCommand command) + { + command.Execute(); + _commands.Add(command); + } + + /// + public void Dispose() + { + _profileEditorService.StopCommandScope(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs index f09f471f4..109714044 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorHistory.cs @@ -7,94 +7,93 @@ using System.Reactive.Subjects; using Artemis.Core; using ReactiveUI; -namespace Artemis.UI.Shared.Services.ProfileEditor +namespace Artemis.UI.Shared.Services.ProfileEditor; + +public class ProfileEditorHistory { - public class ProfileEditorHistory + private readonly Subject _canRedo = new(); + private readonly Subject _canUndo = new(); + private readonly Stack _redoCommands = new(); + private readonly Stack _undoCommands = new(); + + public ProfileEditorHistory(ProfileConfiguration profileConfiguration) { - private readonly Subject _canRedo = new(); - private readonly Subject _canUndo = new(); - private readonly Stack _redoCommands = new(); - private readonly Stack _undoCommands = new(); + ProfileConfiguration = profileConfiguration; - public ProfileEditorHistory(ProfileConfiguration profileConfiguration) - { - ProfileConfiguration = profileConfiguration; + Execute = ReactiveCommand.Create(ExecuteEditorCommand); + Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo); + Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo); + } - Execute = ReactiveCommand.Create(ExecuteEditorCommand); - Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo); - Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo); - } + public ProfileConfiguration ProfileConfiguration { get; } + public IObservable CanUndo => _canUndo.AsObservable().DistinctUntilChanged(); + public IObservable CanRedo => _canRedo.AsObservable().DistinctUntilChanged(); - public ProfileConfiguration ProfileConfiguration { get; } - public IObservable CanUndo => _canUndo.AsObservable().DistinctUntilChanged(); - public IObservable CanRedo => _canRedo.AsObservable().DistinctUntilChanged(); + public ReactiveCommand Execute { get; } + public ReactiveCommand Undo { get; } + public ReactiveCommand Redo { get; } - public ReactiveCommand Execute { get; } - public ReactiveCommand Undo { get; } - public ReactiveCommand Redo { get; } + public void Clear() + { + ClearRedo(); + ClearUndo(); + UpdateSubjects(); + } - public void Clear() - { - ClearRedo(); - ClearUndo(); - UpdateSubjects(); - } + public void ExecuteEditorCommand(IProfileEditorCommand command) + { + command.Execute(); - public void ExecuteEditorCommand(IProfileEditorCommand command) - { - command.Execute(); + _undoCommands.Push(command); + ClearRedo(); + UpdateSubjects(); + } - _undoCommands.Push(command); - ClearRedo(); - UpdateSubjects(); - } + private void ClearRedo() + { + foreach (IProfileEditorCommand profileEditorCommand in _redoCommands) + if (profileEditorCommand is IDisposable disposable) + disposable.Dispose(); - private void ClearRedo() - { - foreach (IProfileEditorCommand profileEditorCommand in _redoCommands) - if (profileEditorCommand is IDisposable disposable) - disposable.Dispose(); + _redoCommands.Clear(); + } - _redoCommands.Clear(); - } + private void ClearUndo() + { + foreach (IProfileEditorCommand profileEditorCommand in _undoCommands) + if (profileEditorCommand is IDisposable disposable) + disposable.Dispose(); - private void ClearUndo() - { - foreach (IProfileEditorCommand profileEditorCommand in _undoCommands) - if (profileEditorCommand is IDisposable disposable) - disposable.Dispose(); + _undoCommands.Clear(); + } - _undoCommands.Clear(); - } + private IProfileEditorCommand? ExecuteUndo() + { + if (!_undoCommands.TryPop(out IProfileEditorCommand? command)) + return null; - private IProfileEditorCommand? ExecuteUndo() - { - if (!_undoCommands.TryPop(out IProfileEditorCommand? command)) - return null; + command.Undo(); + _redoCommands.Push(command); + UpdateSubjects(); - command.Undo(); - _redoCommands.Push(command); - UpdateSubjects(); + return command; + } - return command; - } + private IProfileEditorCommand? ExecuteRedo() + { + if (!_redoCommands.TryPop(out IProfileEditorCommand? command)) + return null; - private IProfileEditorCommand? ExecuteRedo() - { - if (!_redoCommands.TryPop(out IProfileEditorCommand? command)) - return null; + command.Execute(); + _undoCommands.Push(command); + UpdateSubjects(); - command.Execute(); - _undoCommands.Push(command); - UpdateSubjects(); + return command; + } - return command; - } - - private void UpdateSubjects() - { - _canUndo.OnNext(_undoCommands.Any()); - _canRedo.OnNext(_redoCommands.Any()); - } + private void UpdateSubjects() + { + _canUndo.OnNext(_undoCommands.Any()); + _canRedo.OnNext(_redoCommands.Any()); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 339cba6b1..d6f4828db 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using DynamicData; using Serilog; @@ -28,6 +29,7 @@ internal class ProfileEditorService : IProfileEditorService private readonly IProfileService _profileService; private readonly IModuleService _moduleService; private readonly IWindowService _windowService; + private ProfileEditorCommandScope? _profileEditorHistoryScope; public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService) { @@ -86,6 +88,9 @@ internal class ProfileEditorService : IProfileEditorService // Deselect whatever profile element was active ChangeCurrentProfileElement(null); + // Close the command scope if one was open + _profileEditorHistoryScope?.Dispose(); + // The new profile may need activation if (profileConfiguration != null) { @@ -205,11 +210,27 @@ internal class ProfileEditorService : IProfileEditorService return time; } + public TimeSpan RoundTime(TimeSpan time) + { + // Round the time to something that fits the current zoom level + if (_pixelsPerSecondSubject.Value < 50) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 200.0) * 200.0); + if (_pixelsPerSecondSubject.Value < 100) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 100.0) * 100.0); + if (_pixelsPerSecondSubject.Value < 200) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 50.0) * 50.0); + if (_pixelsPerSecondSubject.Value < 500) + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 20.0) * 20.0); + return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds)); + } + public void ChangePixelsPerSecond(int pixelsPerSecond) { _pixelsPerSecondSubject.OnNext(pixelsPerSecond); } + #region Commands + public void ExecuteCommand(IProfileEditorCommand command) { try @@ -218,6 +239,13 @@ internal class ProfileEditorService : IProfileEditorService if (history == null) throw new ArtemisSharedUIException("Can't execute a command when there's no active profile configuration"); + // If a scope is active add the command to it, the scope will execute it immediately + if (_profileEditorHistoryScope != null) + { + _profileEditorHistoryScope.AddCommand(command); + return; + } + history.Execute.Execute(command).Subscribe(); } catch (Exception e) @@ -227,6 +255,36 @@ internal class ProfileEditorService : IProfileEditorService } } + public ProfileEditorCommandScope CreateCommandScope(string name) + { + if (_profileEditorHistoryScope != null) + throw new ArtemisSharedUIException($"A command scope is already active, name: {_profileEditorHistoryScope.Name}."); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + _profileEditorHistoryScope = new ProfileEditorCommandScope(this, name); + return _profileEditorHistoryScope; + } + + internal void StopCommandScope() + { + // This might happen if the scope is disposed twice, it's no biggie + if (_profileEditorHistoryScope == null) + return; + + ProfileEditorCommandScope scope = _profileEditorHistoryScope; + _profileEditorHistoryScope = null; + + // Executing the composite command won't do anything the first time (see last ctor variable) + // commands were already executed each time they were added to the scope + ExecuteCommand(new CompositeCommand(scope.ProfileEditorCommands, scope.Name, true)); + } + + #endregion + + + /// public void SaveProfile() { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs index 58953aed1..602eb1424 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml.cs @@ -72,7 +72,7 @@ public class PropertiesView : ReactiveUserControl if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) { List snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList(); - newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ViewModel.PixelsPerSecond * 5), true, false, snapTimes); + newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, true, false, snapTimes); } // If holding down control, round to the closest 50ms diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs index e90f1a699..c04b6857f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/ITimelineKeyframeViewModel.cs @@ -1,5 +1,6 @@ using System; using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; @@ -12,20 +13,20 @@ public interface ITimelineKeyframeViewModel #region Movement void Select(bool expand, bool toggle); - // void StartMovement(); - // void FinishMovement(); - - void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source); - void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source); - void UpdatePosition(TimeSpan position); - void ReleaseMovement(); + void StartMovement(ITimelineKeyframeViewModel source); + void UpdateMovement(TimeSpan position); + void FinishMovement(); + TimeSpan GetTimeSpanAtPosition(double x); #endregion #region Context menu actions void PopulateEasingViewModels(); - void Delete(bool save = true); + void Duplicate(); + void Copy(); + void Paste(); + void Delete(); #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml index a9f501b03..789084d06 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineKeyframeView.axaml @@ -17,7 +17,8 @@ Classes.selected="{Binding IsSelected}" PointerPressed="InputElement_OnPointerPressed" PointerReleased="InputElement_OnPointerReleased" - PointerMoved="InputElement_OnPointerMoved"> + PointerMoved="InputElement_OnPointerMoved" + Cursor="Hand"> + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml new file mode 100644 index 000000000..aeaa95f4e --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml @@ -0,0 +1,54 @@ + + + + + + + + + + Start + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs new file mode 100644 index 000000000..234acb2de --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml.cs @@ -0,0 +1,50 @@ +using Avalonia.Controls; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments +{ + public partial class StartSegmentView : ReactiveUserControl + { + private readonly Rectangle _keyframeDragAnchor; + private double _dragOffset; + + public StartSegmentView() + { + InitializeComponent(); + _keyframeDragAnchor = this.Get("KeyframeDragAnchor"); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + e.Pointer.Capture(_keyframeDragAnchor); + + _dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X; + ViewModel.StartResize(); + } + + private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e) + { + if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor)) + return; + ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset); + } + + private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor)) + return; + e.Pointer.Capture(null); + ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs new file mode 100644 index 000000000..926466e43 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentViewModel.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Avalonia.Controls.Mixins; +using Castle.DynamicProxy.Generators; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments; + +public class StartSegmentViewModel : TimelineSegmentViewModel +{ + private readonly IProfileEditorService _profileEditorService; + private RenderProfileElement? _profileElement; + private int _pixelsPerSecond; + private TimeSpan _time; + private ObservableAsPropertyHelper? _end; + private ObservableAsPropertyHelper? _endTimestamp; + private readonly ObservableAsPropertyHelper _width; + private TimeSpan _initialLength; + + public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) + { + _profileEditorService = profileEditorService; + this.WhenActivated(d => + { + profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d); + profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d); + profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d); + + _end = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never()) + .Switch() + .CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p) + .ToProperty(this, vm => vm.EndX) + .DisposeWith(d); + _endTimestamp = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never()) + .Switch() + .Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}") + .ToProperty(this, vm => vm.EndTimestamp) + .DisposeWith(d); + }); + + _width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width); + } + + public override TimeSpan Start => TimeSpan.Zero; + public override double StartX => 0; + public override TimeSpan End => _profileElement?.Timeline.StartSegmentEndPosition ?? TimeSpan.Zero; + public override double EndX => _end?.Value ?? 0; + public override TimeSpan Length + { + get => _profileElement?.Timeline.StartSegmentLength ?? TimeSpan.Zero; + set + { + if (_profileElement != null) + _profileElement.Timeline.StartSegmentLength = value; + } + } + + public override double Width => _width.Value; + + public override string? EndTimestamp => _endTimestamp?.Value; + public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start; +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs new file mode 100644 index 000000000..e60369967 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/TimelineSegmentViewModel.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Avalonia.Controls.Mixins; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments; + +public abstract class TimelineSegmentViewModel : ActivatableViewModelBase +{ + private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2); + private readonly IProfileEditorService _profileEditorService; + private RenderProfileElement? _profileElement; + private int _pixelsPerSecond; + private Dictionary _originalKeyframePositions = new(); + + private ObservableAsPropertyHelper? _showAddStart; + private ObservableAsPropertyHelper? _showAddMain; + private ObservableAsPropertyHelper? _showAddEnd; + private TimeSpan _initialLength; + + protected TimelineSegmentViewModel(IProfileEditorService profileEditorService) + { + _profileEditorService = profileEditorService; + this.WhenActivated(d => + { + profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d); + profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d); + + _showAddStart = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentLength) ?? Observable.Never()) + .Switch() + .Select(t => t == TimeSpan.Zero) + .ToProperty(this, vm => vm.ShowAddStart) + .DisposeWith(d); + _showAddMain = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentLength) ?? Observable.Never()) + .Switch() + .Select(t => t == TimeSpan.Zero) + .ToProperty(this, vm => vm.ShowAddMain) + .DisposeWith(d); + _showAddEnd = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentLength) ?? Observable.Never()) + .Switch() + .Select(t => t == TimeSpan.Zero) + .ToProperty(this, vm => vm.ShowAddEnd) + .DisposeWith(d); + }); + } + + public bool ShowAddStart => _showAddStart?.Value ?? false; + public bool ShowAddMain => _showAddMain?.Value ?? false; + public bool ShowAddEnd => _showAddEnd?.Value ?? false; + + public abstract TimeSpan Start { get; } + public abstract double StartX { get; } + public abstract TimeSpan End { get; } + public abstract double EndX { get; } + public abstract TimeSpan Length { get; set; } + public abstract double Width { get; } + public abstract string? EndTimestamp { get; } + public abstract ResizeTimelineSegment.SegmentType Type { get; } + + public void AddStartSegment() + { + if (_profileElement == null) + return; + + using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add start segment"); + ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes), NewSegmentLength); + ApplyPendingKeyframeMovement(); + _profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Start, _profileElement, NewSegmentLength)); + } + + public void AddMainSegment() + { + if (_profileElement == null) + return; + + using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add main segment"); + ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).Where(s => s.Position > _profileElement.Timeline.StartSegmentEndPosition), NewSegmentLength); + ApplyPendingKeyframeMovement(); + _profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Main, _profileElement, NewSegmentLength)); + } + + public void AddEndSegment() + { + if (_profileElement == null) + return; + + using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add end segment"); + _profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.End, _profileElement, NewSegmentLength)); + } + + public void StartResize() + { + if (_profileElement == null) + return; + + _initialLength = Length; + } + + public void UpdateResize(double x) + { + if (_profileElement == null) + return; + + TimeSpan difference = GetTimeFromX(x) - Length; + List keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList(); + ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference); + Length = GetTimeFromX(x); + } + + public void FinishResize(double x) + { + if (_profileElement == null) + return; + + using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment"); + ApplyPendingKeyframeMovement(); + _profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x), _initialLength)); + } + + public void RemoveSegment() + { + if (_profileElement == null) + return; + + using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Remove segment"); + IEnumerable keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes); + + // Delete keyframes in the segment + foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes) + if (layerPropertyKeyframe.Position > Start && layerPropertyKeyframe.Position <= End) + _profileEditorService.ExecuteCommand(new DeleteKeyframe(layerPropertyKeyframe)); + + // Move keyframes after the segment forwards + ShiftKeyframes(keyframes.Where(s => s.Position > End), new TimeSpan(Length.Ticks * -1)); + ApplyPendingKeyframeMovement(); + + _profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero)); + } + + protected TimeSpan GetTimeFromX(double x) + { + TimeSpan length = TimeSpan.FromSeconds(x / _pixelsPerSecond); + if (length < TimeSpan.Zero) + length = TimeSpan.Zero; + return length; + } + + protected void ShiftKeyframes(IEnumerable keyframes, TimeSpan amount) + { + foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes) + { + if (!_originalKeyframePositions.ContainsKey(layerPropertyKeyframe)) + _originalKeyframePositions[layerPropertyKeyframe] = layerPropertyKeyframe.Position; + layerPropertyKeyframe.Position = layerPropertyKeyframe.Position.Add(amount); + } + } + + protected void ApplyPendingKeyframeMovement() + { + foreach ((ILayerPropertyKeyframe keyframe, TimeSpan originalPosition) in _originalKeyframePositions) + _profileEditorService.ExecuteCommand(new MoveKeyframe(keyframe, keyframe.Position, originalPosition)); + + _originalKeyframePositions.Clear(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml index 6a69dbb07..ff04b5406 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyView.axaml @@ -12,8 +12,8 @@ - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs index d908f3e02..354b7c30b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelinePropertyViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia.Controls.Mixins; @@ -24,6 +25,9 @@ public class TimelinePropertyViewModel : ActivatableViewModelBase, ITimelineP this.WhenActivated(d => { + Observable.FromEventPattern(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x) + .Subscribe(_ => UpdateKeyframes()) + .DisposeWith(d); Observable.FromEventPattern(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x) .Subscribe(_ => UpdateKeyframes()) .DisposeWith(d); diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml index 44674b0c9..0dbaf8c3b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml @@ -10,8 +10,11 @@ 28 29 - - + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs index c311cf428..c23f6e109 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes; using Artemis.UI.Shared.Controls; using Artemis.UI.Shared.Events; using Artemis.UI.Shared.Extensions; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs index 4452f5906..ccd0aa2b7 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineViewModel.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes; +using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; @@ -17,10 +20,18 @@ public class TimelineViewModel : ActivatableViewModelBase private ObservableAsPropertyHelper? _caretPosition; private ObservableAsPropertyHelper? _pixelsPerSecond; private List? _moveKeyframes; + private ObservableAsPropertyHelper _minWidth; - public TimelineViewModel(ObservableCollection propertyGroupViewModels, IProfileEditorService profileEditorService) + public TimelineViewModel(ObservableCollection propertyGroupViewModels, + StartSegmentViewModel startSegmentViewModel, + MainSegmentViewModel mainSegmentViewModel, + EndSegmentViewModel endSegmentViewModel, + IProfileEditorService profileEditorService) { PropertyGroupViewModels = propertyGroupViewModels; + StartSegmentViewModel = startSegmentViewModel; + MainSegmentViewModel = mainSegmentViewModel; + EndSegmentViewModel = endSegmentViewModel; _profileEditorService = profileEditorService; this.WhenActivated(d => @@ -29,14 +40,24 @@ public class TimelineViewModel : ActivatableViewModelBase .CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p) .ToProperty(this, vm => vm.CaretPosition) .DisposeWith(d); - _pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); + _minWidth = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never()) + .Switch() + .CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100) + .ToProperty(this, vm => vm.MinWidth) + .DisposeWith(d); }); } public ObservableCollection PropertyGroupViewModels { get; } + public StartSegmentViewModel StartSegmentViewModel { get; } + public MainSegmentViewModel MainSegmentViewModel { get; } + public EndSegmentViewModel EndSegmentViewModel { get; } + public double CaretPosition => _caretPosition?.Value ?? 0.0; public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0; + public double MinWidth => _minWidth?.Value ?? 0; public void ChangeTime(TimeSpan newTime) { @@ -118,10 +139,10 @@ public class TimelineViewModel : ActivatableViewModelBase #region Keyframe actions - public void DuplicateKeyframes(ITimelineKeyframeViewModel source) + public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null) { - if (!source.IsSelected) - source.Duplicate(); + if (source is { IsSelected: false }) + source.Delete(); else { List keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList(); @@ -131,9 +152,9 @@ public class TimelineViewModel : ActivatableViewModelBase } } - public void CopyKeyframes(ITimelineKeyframeViewModel source) + public void CopyKeyframes(ITimelineKeyframeViewModel? source = null) { - if (!source.IsSelected) + if (source is { IsSelected: false }) source.Copy(); else { @@ -144,9 +165,9 @@ public class TimelineViewModel : ActivatableViewModelBase } } - public void PasteKeyframes(ITimelineKeyframeViewModel source) + public void PasteKeyframes(ITimelineKeyframeViewModel? source = null) { - if (!source.IsSelected) + if (source is { IsSelected: false }) source.Paste(); else { @@ -157,9 +178,9 @@ public class TimelineViewModel : ActivatableViewModelBase } } - public void DeleteKeyframes(ITimelineKeyframeViewModel source) + public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null) { - if (!source.IsSelected) + if (source is {IsSelected: false}) source.Delete(); else { From 76ef542e18247638accb980e8dfd49d55847e35d Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 26 Jan 2022 22:34:36 +0100 Subject: [PATCH 122/270] Profile editor - Store panel sizes as settings --- .../Commands/UpdateLayerProperty.cs | 16 ++++++++-- .../Converters/DoubleToGridLengthConverter.cs | 29 +++++++++++++++++++ .../Panels/Properties/PropertiesView.axaml | 15 ++++++++-- .../Panels/Properties/PropertiesViewModel.cs | 16 ++++++---- .../ProfileEditor/ProfileEditorView.axaml | 25 ++++++++++++++-- .../ProfileEditor/ProfileEditorViewModel.cs | 8 ++++- 6 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 src/Avalonia/Artemis.UI/Converters/DoubleToGridLengthConverter.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs index d4dc4a800..d6179bb4e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs @@ -23,6 +23,8 @@ public class UpdateLayerProperty : IProfileEditorCommand _originalValue = layerProperty.CurrentValue; _newValue = newValue; _time = time; + + DisplayName = $"Update {_layerProperty.PropertyDescription.Name ?? "property"}"; } /// @@ -34,17 +36,27 @@ public class UpdateLayerProperty : IProfileEditorCommand _originalValue = originalValue; _newValue = newValue; _time = time; + + DisplayName = $"Update {_layerProperty.PropertyDescription.Name ?? "property"}"; } #region Implementation of IProfileEditorCommand /// - public string DisplayName => $"Update {_layerProperty.PropertyDescription.Name ?? "property"}"; + public string DisplayName { get; private set; } /// public void Execute() { - _newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time); + // If there was already a keyframe from a previous execute that was undone, put it back + if (_newKeyframe != null) + _layerProperty.AddKeyframe(_newKeyframe); + else + { + _newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time); + if (_newKeyframe != null) + DisplayName = $"Add {_layerProperty.PropertyDescription.Name ?? "property"} keyframe"; + } } /// diff --git a/src/Avalonia/Artemis.UI/Converters/DoubleToGridLengthConverter.cs b/src/Avalonia/Artemis.UI/Converters/DoubleToGridLengthConverter.cs new file mode 100644 index 000000000..f8077eb57 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/DoubleToGridLengthConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Converters; + +public class DoubleToGridLengthConverter : IValueConverter +{ + #region Implementation of IValueConverter + + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is double doubleValue) + return new GridLength(doubleValue, GridUnitType.Pixel); + return new GridLength(1, GridUnitType.Star); + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is GridLength gridLength) + return gridLength.Value; + return 0.0; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml index 4fe95180c..5b694742c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml @@ -4,12 +4,21 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:Artemis.UI.Controls" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + xmlns:converters="clr-namespace:Artemis.UI.Converters" + mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="350" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView"> - + + + + + + + + + @@ -97,7 +106,7 @@ - _cachedViewModels; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly IProfileEditorService _profileEditorService; + private readonly ISettingsService _settingsService; private ObservableAsPropertyHelper? _pixelsPerSecond; private ObservableAsPropertyHelper? _profileElement; /// - public PropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel) + public PropertiesViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel) { _profileEditorService = profileEditorService; + _settingsService = settingsService; _layerPropertyVmFactory = layerPropertyVmFactory; - PropertyGroupViewModels = new ObservableCollection(); _cachedViewModels = new Dictionary(); + PropertyGroupViewModels = new ObservableCollection(); PlaybackViewModel = playbackViewModel; TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels); @@ -54,19 +57,22 @@ public class PropertiesViewModel : ActivatableViewModelBase { _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); _pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); + Disposable.Create(() => _settingsService.SaveAllSettings()).DisposeWith(d); }); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups()); } + public ObservableCollection PropertyGroupViewModels { get; } public PlaybackViewModel PlaybackViewModel { get; } public TimelineViewModel TimelineViewModel { get; } + public RenderProfileElement? ProfileElement => _profileElement?.Value; public Layer? Layer => _profileElement?.Value as Layer; + public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0; public IObservable Playing => _profileEditorService.Playing; - - public ObservableCollection PropertyGroupViewModels { get; } - + public PluginSetting PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0); + private void UpdateGroups() { if (ProfileElement == null) diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index f0a87752a..056002025 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -4,8 +4,12 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileEditorView"> + + + @@ -35,13 +39,23 @@ - + + + + + + - + + + + + + @@ -73,7 +87,12 @@ - + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index e75dfb043..81919ca36 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Screens.ProfileEditor.MenuBar; using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.Properties; @@ -14,13 +15,14 @@ namespace Artemis.UI.Screens.ProfileEditor { public class ProfileEditorViewModel : MainScreenViewModel { + private readonly ISettingsService _settingsService; private ObservableAsPropertyHelper? _profileConfiguration; private ObservableAsPropertyHelper? _history; /// public ProfileEditorViewModel(IScreen hostScreen, - IKernel kernel, IProfileEditorService profileEditorService, + ISettingsService settingsService, VisualEditorViewModel visualEditorViewModel, ProfileTreeViewModel profileTreeViewModel, ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, @@ -29,6 +31,7 @@ namespace Artemis.UI.Screens.ProfileEditor StatusBarViewModel statusBarViewModel) : base(hostScreen, "profile-editor") { + _settingsService = settingsService; VisualEditorViewModel = visualEditorViewModel; ProfileTreeViewModel = profileTreeViewModel; PropertiesViewModel = propertiesViewModel; @@ -51,6 +54,9 @@ namespace Artemis.UI.Screens.ProfileEditor public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public ProfileEditorHistory? History => _history?.Value; + public PluginSetting TreeWidth => _settingsService.GetSetting("ProfileEditor.TreeWidth", 350.0); + public PluginSetting ConditionsHeight => _settingsService.GetSetting("ProfileEditor.ConditionsHeight", 300.0); + public PluginSetting PropertiesHeight => _settingsService.GetSetting("ProfileEditor.PropertiesHeight", 300.0); public void OpenUrl(string url) { From 9ebdaec4f1e0fb64314da67a26d8c299feffd996 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 29 Jan 2022 00:21:42 +0100 Subject: [PATCH 123/270] TextBox - Added prefix and suffix attached properties NumberBox - Added prefix and suffix attached properties Profile editor - Use compiled bindings where applicable --- .../AttachedProperties/NumberBoxAssist.cs | 55 ++++++++ .../AttachedProperties/TextBoxAssist.cs | 65 ++++++++++ .../PropertyInput/PropertyInputViewModel.cs | 11 +- .../Artemis.UI.Shared/Styles/Artemis.axaml | 2 + .../Artemis.UI.Shared/Styles/NumberBox.axaml | 120 +++++++++++++++++ .../Artemis.UI.Shared/Styles/TextBox.axaml | 122 ++++++++++++++++++ src/Avalonia/Artemis.UI/Artemis.UI.csproj | 3 + .../Display/SKColorDataModelDisplayView.xaml | 64 --------- .../SKColorDataModelDisplayViewModel.cs | 8 -- .../PropertyInput/BoolPropertyInputView.axaml | 9 +- .../BrushPropertyInputView.axaml | 16 ++- .../FloatPropertyInputView.axaml | 25 ++-- .../PropertyInput/IntPropertyInputView.axaml | 23 ++-- .../SKColorPropertyInputView.axaml | 35 +++-- .../SKPointPropertyInputView.axaml | 26 ++-- .../SKSizePropertyInputView.axaml | 26 ++-- .../Panels/Playback/PlaybackView.axaml | 24 ++-- .../ProfileTree/FolderTreeItemView.axaml | 16 ++- .../ProfileTree/LayerTreeItemView.axaml | 18 +-- .../Panels/ProfileTree/ProfileTreeView.axaml | 36 +++--- .../Panels/ProfileTree/TreeItemViewModel.cs | 7 + .../Panels/Properties/PropertiesView.axaml | 63 ++++----- .../Keyframes/TimelineEasingView.axaml | 8 +- .../Timeline/Segments/EndSegmentView.axaml | 10 +- .../Timeline/Segments/MainSegmentView.axaml | 16 ++- .../Timeline/Segments/MainSegmentViewModel.cs | 22 ++-- .../Timeline/Segments/StartSegmentView.axaml | 10 +- .../Timeline/TimelineGroupView.axaml | 23 ++-- .../Timeline/TimelineGroupViewModel.cs | 10 +- .../Properties/Timeline/TimelineView.axaml | 12 +- .../Properties/Tree/TreePropertyView.axaml | 2 + .../VisualEditor/VisualEditorView.axaml | 10 +- .../ProfileEditor/ProfileEditorView.axaml | 24 ++-- .../Screens/Workshop/WorkshopView.axaml | 20 ++- 34 files changed, 651 insertions(+), 290 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/AttachedProperties/NumberBoxAssist.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/AttachedProperties/TextBoxAssist.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Styles/NumberBox.axaml create mode 100644 src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml delete mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml delete mode 100644 src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs diff --git a/src/Avalonia/Artemis.UI.Shared/AttachedProperties/NumberBoxAssist.cs b/src/Avalonia/Artemis.UI.Shared/AttachedProperties/NumberBoxAssist.cs new file mode 100644 index 000000000..91d873df0 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/AttachedProperties/NumberBoxAssist.cs @@ -0,0 +1,55 @@ +using System.Windows.Input; +using Avalonia; +using FluentAvalonia.UI.Controls; + +namespace Artemis.UI.Shared.AttachedProperties; + +/// +/// Helper properties for working with NumberBoxes. +/// +public class NumberBoxAssist : AvaloniaObject +{ + /// + /// Identifies the Avalonia attached property. + /// + /// Provide an derived object or binding. + public static readonly AttachedProperty SuffixTextProperty = AvaloniaProperty.RegisterAttached("SuffixText", typeof(NumberBox)); + + /// + /// Identifies the Avalonia attached property. + /// + /// Provide an derived object or binding. + public static readonly AttachedProperty PrefixTextProperty = AvaloniaProperty.RegisterAttached("PrefixText", typeof(NumberBox)); + + /// + /// Accessor for Attached property . + /// + public static void SetSuffixText(AvaloniaObject element, string value) + { + element.SetValue(SuffixTextProperty, value); + } + + /// + /// Accessor for Attached property . + /// + public static string GetSuffixText(AvaloniaObject element) + { + return element.GetValue(SuffixTextProperty); + } + + /// + /// Accessor for Attached property . + /// + public static void SetPrefixText(AvaloniaObject element, string value) + { + element.SetValue(PrefixTextProperty, value); + } + + /// + /// Accessor for Attached property . + /// + public static string GetPrefixText(AvaloniaObject element) + { + return element.GetValue(PrefixTextProperty); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/AttachedProperties/TextBoxAssist.cs b/src/Avalonia/Artemis.UI.Shared/AttachedProperties/TextBoxAssist.cs new file mode 100644 index 000000000..7a822a99d --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/AttachedProperties/TextBoxAssist.cs @@ -0,0 +1,65 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; + +namespace Artemis.UI.Shared.AttachedProperties; + +/// +/// Helper properties for working with TextBoxes. +/// +public class TextBoxAssist : AvaloniaObject +{ + /// + /// Identifies the Avalonia attached property. + /// + /// Provide an derived object or binding. + public static readonly AttachedProperty SuffixTextProperty = AvaloniaProperty.RegisterAttached("SuffixText", typeof(TextBox)); + + /// + /// Identifies the Avalonia attached property. + /// + /// Provide an derived object or binding. + public static readonly AttachedProperty PrefixTextProperty = AvaloniaProperty.RegisterAttached("PrefixText", typeof(TextBox)); + + /// + /// Accessor for Attached property . + /// + public static void SetSuffixText(AvaloniaObject element, string value) + { + element.SetValue(SuffixTextProperty, value); + + if (!string.IsNullOrWhiteSpace(value) && !((TextBox)element).Classes.Contains("suffixed")) + ((TextBox)element).Classes.Add("suffixed"); + else + ((TextBox)element).Classes.Remove("suffixed"); + } + + /// + /// Accessor for Attached property . + /// + public static string GetSuffixText(AvaloniaObject element) + { + return element.GetValue(SuffixTextProperty); + } + + /// + /// Accessor for Attached property . + /// + public static void SetPrefixText(AvaloniaObject element, string value) + { + element.SetValue(PrefixTextProperty, value); + + if (!string.IsNullOrWhiteSpace(value) && !((TextBox)element).Classes.Contains("prefixed")) + ((TextBox)element).Classes.Add("prefixed"); + else + ((TextBox)element).Classes.Remove("prefixed"); + } + + /// + /// Accessor for Attached property . + /// + public static string GetPrefixText(AvaloniaObject element) + { + return element.GetValue(PrefixTextProperty); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs index 0b2e9f253..f36cf3a51 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs @@ -103,6 +103,16 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel } } + /// + /// Gets the prefix to show before input elements + /// + public string? Prefix => LayerProperty.PropertyDescription.InputPrefix; + + /// + /// Gets the affix to show after input elements + /// + public string? Affix => LayerProperty.PropertyDescription.InputAffix; + internal override object InternalGuard { get; } = new(); /// @@ -186,7 +196,6 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel { _updating = false; } - } private void UpdateDataBinding() diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index 302197215..f77636350 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -28,6 +28,8 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml new file mode 100644 index 000000000..e536cf635 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Styles/TextBox.axaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 21e8cea0e..f3ea192ba 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -54,4 +54,7 @@ PropertiesView.axaml + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml b/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml deleted file mode 100644 index e6215d1a6..000000000 --- a/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayView.xaml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs deleted file mode 100644 index c08de2224..000000000 --- a/src/Avalonia/Artemis.UI/DefaultTypes/DataModel/Display/SKColorDataModelDisplayViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Artemis.UI.Shared.DataModelVisualization; -using SkiaSharp; - -namespace Artemis.UI.DefaultTypes.DataModel.Display; - -public class SKColorDataModelDisplayViewModel : DataModelDisplayViewModel -{ -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml index 8452fec0f..8912a6a9b 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BoolPropertyInputView.axaml @@ -3,7 +3,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" + xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.DefaultTypes.PropertyInput.BoolPropertyInputView"> - + x:Class="Artemis.UI.DefaultTypes.PropertyInput.BoolPropertyInputView" + x:DataType="propertyInput:BoolPropertyInputViewModel"> + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index ec3832869..8407e5714 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -4,8 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:layerBrushes="clr-namespace:Artemis.Core.LayerBrushes;assembly=Artemis.Core" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.DefaultTypes.PropertyInput.BrushPropertyInputView"> + x:Class="Artemis.UI.DefaultTypes.PropertyInput.BrushPropertyInputView" + x:DataType="propertyInput:BrushPropertyInputViewModel"> - - - - - + + + + + - + - + - + - + - + - + - + - + @@ -68,10 +70,10 @@ - - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 7b79ca8b5..b24e33450 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -68,6 +68,10 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree RenameValue = ProfileElement?.Name; }); + Duplicate = ReactiveCommand.Create(() => throw new NotImplementedException()); + Copy = ReactiveCommand.Create(() => throw new NotImplementedException()); + Paste = ReactiveCommand.Create(() => throw new NotImplementedException()); + Delete = ReactiveCommand.Create(() => { if (ProfileElement is RenderProfileElement renderProfileElement) @@ -106,6 +110,9 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public ReactiveCommand AddLayer { get; } public ReactiveCommand AddFolder { get; } public ReactiveCommand Rename { get; } + public ReactiveCommand Duplicate { get; } + public ReactiveCommand Copy { get; } + public ReactiveCommand Paste { get; } public ReactiveCommand Delete { get; } public string? RenameValue diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml index 5b694742c..1e2461687 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertiesView.axaml @@ -6,7 +6,8 @@ xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties" xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="350" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView" + x:DataType="local:PropertiesViewModel"> @@ -15,22 +16,22 @@ - + - + - + - - + + @@ -52,53 +53,53 @@ Margin="0 18 0 0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" HorizontalAlignment="Left" - PixelsPerSecond="{Binding PixelsPerSecond}" - HorizontalOffset="{Binding #TimelineScrollViewer.Offset.X, Mode=OneWay}" - VisibleWidth="{Binding #TimelineScrollViewer.Bounds.Width}" + PixelsPerSecond="{CompiledBinding PixelsPerSecond}" + HorizontalOffset="{CompiledBinding #TimelineScrollViewer.Offset.X, Mode=OneWay}" + VisibleWidth="{CompiledBinding #TimelineScrollViewer.Bounds.Width}" OffsetFirstValue="True" PointerReleased="TimelineHeader_OnPointerReleased" - Width="{Binding #TimelineScrollViewer.Viewport.Width}" + Width="{CompiledBinding #TimelineScrollViewer.Viewport.Width}" Cursor="Hand" /> - - + - + + Content="{CompiledBinding TimelineViewModel.StartSegmentViewModel}" /> - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml index bcc8e697e..4dace623e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineEasingView.axaml @@ -2,16 +2,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:keyframes="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView" + x:DataType="keyframes:TimelineEasingViewModel"> - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml index bb15969fa..85cd1dcae 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/EndSegmentView.axaml @@ -3,15 +3,17 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView" + x:DataType="segments:EndSegmentViewModel"> + IsVisible="{CompiledBinding ShowAddMain}"> @@ -50,7 +52,7 @@ PointerPressed="KeyframeDragAnchor_OnPointerPressed" PointerMoved="KeyframeDragAnchor_OnPointerMoved" PointerReleased="KeyframeDragAnchor_OnPointerReleased" - ToolTip.Tip="{Binding EndTimestamp}"/> + ToolTip.Tip="{CompiledBinding EndTimestamp}"/> \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml index 6efe166ab..9884a7533 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentView.axaml @@ -3,20 +3,22 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView" + x:DataType="segments:MainSegmentViewModel"> @@ -47,7 +49,7 @@ @@ -59,7 +61,7 @@ VerticalAlignment="Center" ToolTip.Tip="Add an end segment" Command="{Binding AddEndSegment}" - IsVisible="{Binding ShowAddEnd}"> + IsVisible="{CompiledBinding ShowAddEnd}"> @@ -72,7 +74,7 @@ PointerPressed="KeyframeDragAnchor_OnPointerPressed" PointerMoved="KeyframeDragAnchor_OnPointerMoved" PointerReleased="KeyframeDragAnchor_OnPointerReleased" - ToolTip.Tip="{Binding EndTimestamp}"/> + ToolTip.Tip="{CompiledBinding EndTimestamp}"/> \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs index bad569d7b..0d3eb4c5a 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/MainSegmentViewModel.cs @@ -13,24 +13,18 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments; public class MainSegmentViewModel : TimelineSegmentViewModel { - private readonly IProfileEditorService _profileEditorService; private RenderProfileElement? _profileElement; - private int _pixelsPerSecond; - private TimeSpan _time; private ObservableAsPropertyHelper? _start; private ObservableAsPropertyHelper? _end; private ObservableAsPropertyHelper? _endTimestamp; + private ObservableAsPropertyHelper? _repeatSegment; private readonly ObservableAsPropertyHelper _width; - private TimeSpan _initialLength; public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) { - _profileEditorService = profileEditorService; this.WhenActivated(d => { profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d); - profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d); - profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d); _start = profileEditorService.ProfileElement .Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentStartPosition) ?? Observable.Never()) @@ -50,6 +44,12 @@ public class MainSegmentViewModel : TimelineSegmentViewModel .Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}") .ToProperty(this, vm => vm.EndTimestamp) .DisposeWith(d); + _repeatSegment = profileEditorService.ProfileElement + .Select(p => p?.WhenAnyValue(element => element.Timeline.PlayMode) ?? Observable.Never()) + .Switch() + .Select(p => p == TimelinePlayMode.Repeat) + .ToProperty(this, vm => vm.RepeatSegment) + .DisposeWith(d); }); _width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width); @@ -59,6 +59,7 @@ public class MainSegmentViewModel : TimelineSegmentViewModel public override double StartX => _start?.Value ?? 0; public override TimeSpan End => _profileElement?.Timeline.MainSegmentEndPosition ?? TimeSpan.Zero; public override double EndX => _end?.Value ?? 0; + public override TimeSpan Length { get => _profileElement?.Timeline.MainSegmentLength ?? TimeSpan.Zero; @@ -70,7 +71,12 @@ public class MainSegmentViewModel : TimelineSegmentViewModel } public override double Width => _width.Value; - public override string? EndTimestamp => _endTimestamp?.Value; public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main; + + public bool RepeatSegment + { + get => _repeatSegment?.Value ?? false; + set => throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml index aeaa95f4e..8ec723260 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/StartSegmentView.axaml @@ -4,15 +4,17 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView" + x:DataType="segments:StartSegmentViewModel"> @@ -34,7 +36,7 @@ Classes="AppBarButton icon-button icon-button-small" ToolTip.Tip="Add main segment" Command="{Binding AddMainSegment}" - IsVisible="{Binding ShowAddMain}"> + IsVisible="{CompiledBinding ShowAddMain}"> @@ -47,7 +49,7 @@ PointerPressed="KeyframeDragAnchor_OnPointerPressed" PointerMoved="KeyframeDragAnchor_OnPointerMoved" PointerReleased="KeyframeDragAnchor_OnPointerReleased" - ToolTip.Tip="{Binding EndTimestamp}"/> + ToolTip.Tip="{CompiledBinding EndTimestamp}"/> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupView.axaml index 22bc2b8fb..b63e8f7bb 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupView.axaml @@ -3,18 +3,15 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:properties="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties" + xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineGroupView"> - - - - - - + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineGroupView" + x:DataType="timeline:TimelineGroupViewModel"> + @@ -41,15 +38,15 @@ - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs index e6ea9e6d4..a78dace60 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs @@ -10,7 +10,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline; public class TimelineGroupViewModel : ActivatableViewModelBase { - private ObservableCollection? _keyframePosition; + private ObservableCollection? _keyframePositions; private int _pixelsPerSecond; public TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IProfileEditorService profileEditorService) @@ -33,15 +33,15 @@ public class TimelineGroupViewModel : ActivatableViewModelBase public ObservableCollection? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null; - public ObservableCollection? KeyframePosition + public ObservableCollection? KeyframePositions { - get => _keyframePosition; - set => this.RaiseAndSetIfChanged(ref _keyframePosition, value); + get => _keyframePositions; + set => this.RaiseAndSetIfChanged(ref _keyframePositions, value); } private void UpdateKeyframePositions() { - KeyframePosition = new ObservableCollection(PropertyGroupViewModel + KeyframePositions = new ObservableCollection(PropertyGroupViewModel .GetAllKeyframeViewModels(false) .Select(p => p.Position.TotalSeconds * _pixelsPerSecond)); } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml index 0dbaf8c3b..b01eacb4e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineView.axaml @@ -4,8 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" + xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineView"> + x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineView" + x:DataType="timeline:TimelineViewModel"> 28 29 @@ -14,14 +16,14 @@ - + - - + + - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml index edda3d7d9..18b4a529d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml @@ -41,6 +41,7 @@ ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}" /> - public void UpdateTimeline(double deltaTime) + protected void UpdateTimeline(double deltaTime) { // TODO: Move to conditions @@ -327,7 +327,7 @@ namespace Artemis.Core OrderEffects(); } - + internal void ActivateLayerEffect(BaseLayerEffect layerEffect) { @@ -406,5 +406,12 @@ namespace Artemis.Core } #endregion + + /// + /// Overrides the main timeline to the specified time and clears any extra time lines + /// + /// The position to set the timeline to + /// Whether to stick to the main segment, wrapping around if needed + public abstract void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs index ef4bf2740..4e17d746f 100644 --- a/src/Artemis.Core/Models/Profile/Timeline.cs +++ b/src/Artemis.Core/Models/Profile/Timeline.cs @@ -11,7 +11,6 @@ namespace Artemis.Core; /// public class Timeline : CorePropertyChanged, IStorageModel { - private const int MaxExtraTimelines = 15; private readonly object _lock = new(); /// @@ -21,78 +20,33 @@ public class Timeline : CorePropertyChanged, IStorageModel { Entity = new TimelineEntity(); MainSegmentLength = TimeSpan.FromSeconds(5); - - _extraTimelines = new List(); - ExtraTimelines = new ReadOnlyCollection(_extraTimelines); - + Save(); } internal Timeline(TimelineEntity entity) { Entity = entity; - _extraTimelines = new List(); - ExtraTimelines = new ReadOnlyCollection(_extraTimelines); Load(); } - private Timeline(Timeline parent) - { - Entity = new TimelineEntity(); - Parent = parent; - StartSegmentLength = Parent.StartSegmentLength; - MainSegmentLength = Parent.MainSegmentLength; - EndSegmentLength = Parent.EndSegmentLength; - - _extraTimelines = new List(); - ExtraTimelines = new ReadOnlyCollection(_extraTimelines); - } - /// public override string ToString() { return $"Progress: {Position}/{Length} - delta: {Delta}"; } - - #region Extra timelines - - /// - /// Adds an extra timeline to this timeline - /// - public void AddExtraTimeline() - { - _extraTimelines.Add(new Timeline(this)); - if (_extraTimelines.Count > MaxExtraTimelines) - _extraTimelines.RemoveAt(0); - } - - /// - /// Removes all extra timelines from this timeline - /// - public void ClearExtraTimelines() - { - _extraTimelines.Clear(); - } - - #endregion - + #region Properties private TimeSpan _position; private TimeSpan _lastDelta; private TimelinePlayMode _playMode; private TimelineStopMode _stopMode; - private readonly List _extraTimelines; private TimeSpan _startSegmentLength; private TimeSpan _mainSegmentLength; private TimeSpan _endSegmentLength; - - /// - /// Gets the parent this timeline is an extra timeline of - /// - public Timeline? Parent { get; } - + /// /// Gets the current position of the timeline /// @@ -105,21 +59,13 @@ public class Timeline : CorePropertyChanged, IStorageModel /// /// Gets the cumulative delta of all calls to that took place after the last call to /// - /// - /// Note: If this is an extra timeline is always equal to - /// /// public TimeSpan Delta { - get => Parent == null ? _lastDelta : DeltaToParent; + get => _lastDelta; private set => SetAndNotify(ref _lastDelta, value); } - - /// - /// Gets the delta to this timeline's - /// - public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero; - + /// /// Gets or sets the mode in which the render element starts its timeline when display conditions are met /// @@ -138,15 +84,10 @@ public class Timeline : CorePropertyChanged, IStorageModel set => SetAndNotify(ref _stopMode, value); } - /// - /// Gets a list of extra copies of the timeline applied to this timeline - /// - public ReadOnlyCollection ExtraTimelines { get; } - /// /// Gets a boolean indicating whether the timeline has finished its run /// - public bool IsFinished => Position > Length && !ExtraTimelines.Any(); + public bool IsFinished => Position > Length; /// /// Gets a boolean indicating whether the timeline progress has been overridden @@ -327,19 +268,15 @@ public class Timeline : CorePropertyChanged, IStorageModel IsOverridden = false; _lastOverridePosition = Position; - if (stickToMainSegment && Position > MainSegmentEndPosition) - { - // If the main segment has no length, simply stick to the start of the segment - if (MainSegmentLength == TimeSpan.Zero) - Position = MainSegmentStartPosition; - // Ensure wrapping back around retains the delta time - else - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); - } + if (!stickToMainSegment || Position <= MainSegmentEndPosition) + return; - _extraTimelines.RemoveAll(t => t.IsFinished); - foreach (Timeline extraTimeline in _extraTimelines) - extraTimeline.Update(delta, false); + // If the main segment has no length, simply stick to the start of the segment + if (MainSegmentLength == TimeSpan.Zero) + Position = MainSegmentStartPosition; + // Ensure wrapping back around retains the delta time + else + Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); } } @@ -389,11 +326,11 @@ public class Timeline : CorePropertyChanged, IStorageModel } /// - /// Overrides the to the specified time and clears any extra time lines + /// Overrides the to the specified time /// /// The position to set the timeline to /// Whether to stick to the main segment, wrapping around if needed - public void Override(TimeSpan position, bool stickToMainSegment) + internal void Override(TimeSpan position, bool stickToMainSegment) { lock (_lock) { @@ -403,24 +340,22 @@ public class Timeline : CorePropertyChanged, IStorageModel IsOverridden = true; _lastOverridePosition = position; - if (stickToMainSegment && Position >= MainSegmentStartPosition) - { - bool atSegmentStart = Position == MainSegmentStartPosition; - if (MainSegmentLength > TimeSpan.Zero) - { - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); - // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor - // is actually at the start of the segment - if (Position == MainSegmentStartPosition && !atSegmentStart) - Position = MainSegmentEndPosition; - } - else - { - Position = MainSegmentStartPosition; - } - } + if (!stickToMainSegment || Position < MainSegmentStartPosition) + return; - _extraTimelines.Clear(); + bool atSegmentStart = Position == MainSegmentStartPosition; + if (MainSegmentLength > TimeSpan.Zero) + { + Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); + // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor + // is actually at the start of the segment + if (Position == MainSegmentStartPosition && !atSegmentStart) + Position = MainSegmentEndPosition; + } + else + { + Position = MainSegmentStartPosition; + } } } diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 19118d88f..68d1782f0 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -146,10 +146,11 @@ namespace Artemis.UI.Shared.Services else { renderElement.Enable(); - renderElement.Timeline.Override( + renderElement.OverrideTimelineAndApply( CurrentTime, (renderElement != SelectedProfileElement || renderElement.Timeline.Length < CurrentTime) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat ); + renderElement.Update(0); foreach (ProfileElement child in renderElement.Children) TickProfileElement(child); diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 1c52211dd..858fb5427 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -79,10 +79,8 @@ internal class ProfileEditorService : IProfileEditorService else { renderElement.Enable(); - renderElement.Timeline.Override( - time, - (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat - ); + bool stickToMainSegment = (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat; + renderElement.OverrideTimelineAndApply(time, stickToMainSegment); foreach (ProfileElement child in renderElement.Children) TickProfileElement(child, time); diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 0de0c2b5d..f53a9a426 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -56,6 +56,9 @@ PropertiesView.axaml + + LayerShapeVisualizerView.axaml + diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 7f7eaa297..b7163bcd3 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -65,6 +65,7 @@ namespace Artemis.UI.Ninject.Factories ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen); FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder); LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer); + LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer); LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer); } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml index dcf5a3b7f..dfa802ef5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml @@ -48,6 +48,12 @@ + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 5169bca53..1ad1fa430 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive.Disposables; using Artemis.Core; @@ -8,6 +9,8 @@ using Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; +using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor; @@ -16,14 +19,22 @@ public class VisualEditorViewModel : ActivatableViewModelBase { private readonly IProfileEditorVmFactory _vmFactory; private ObservableAsPropertyHelper? _profileConfiguration; + private readonly SourceList _visualizers; public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory) { _vmFactory = vmFactory; + _visualizers = new SourceList(); + Devices = new ObservableCollection(rgbService.EnabledDevices); - Visualizers = new ObservableCollection(); Tools = new ObservableCollection(); + _visualizers.Connect() + .Sort(SortExpressionComparer.Ascending(vm => vm.Order)) + .Bind(out ReadOnlyObservableCollection visualizers) + .Subscribe(); + Visualizers = visualizers; + this.WhenActivated(d => { _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); @@ -34,21 +45,24 @@ public class VisualEditorViewModel : ActivatableViewModelBase public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public ObservableCollection Devices { get; } - public ObservableCollection Visualizers { get; set; } - public ObservableCollection Tools { get; set; } + public ReadOnlyObservableCollection Visualizers { get; } + public ObservableCollection Tools { get; } private void CreateVisualizers(ProfileConfiguration? profileConfiguration) { - Visualizers.Clear(); - if (profileConfiguration?.Profile == null) - return; - - foreach (Layer layer in profileConfiguration.Profile.GetAllLayers()) - CreateVisualizer(layer); + _visualizers.Edit(list => + { + list.Clear(); + if (profileConfiguration?.Profile == null) + return; + foreach (Layer layer in profileConfiguration.Profile.GetAllLayers()) + CreateVisualizer(list, layer); + }); } - private void CreateVisualizer(Layer layer) + private void CreateVisualizer(ICollection visualizerViewModels, Layer layer) { - Visualizers.Add(_vmFactory.LayerVisualizerViewModel(layer)); + visualizerViewModels.Add(_vmFactory.LayerShapeVisualizerViewModel(layer)); + visualizerViewModels.Add(_vmFactory.LayerVisualizerViewModel(layer)); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs index 843d32114..385fab60f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs @@ -2,4 +2,7 @@ public interface IVisualizerViewModel { + int X { get; } + int Y { get; } + int Order { get; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml new file mode 100644 index 000000000..0fe4b62bb --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs new file mode 100644 index 000000000..cbef48437 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers +{ + public partial class LayerShapeVisualizerView : ReactiveUserControl + { + public LayerShapeVisualizerView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs new file mode 100644 index 000000000..ea3baee08 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs @@ -0,0 +1,78 @@ +using System; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; +using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia; +using Avalonia.Controls.Mixins; +using Avalonia.Media; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; + +public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel +{ + private ObservableAsPropertyHelper? _selected; + private Geometry? _shapeGeometry; + + public LayerShapeVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService) + { + Layer = layer; + + this.WhenActivated(d => + { + Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x).Subscribe(_ => Update()).DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.Position.CurrentValueSet += x, x => Layer.Transform.Position.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.Rotation.CurrentValueSet += x, x => Layer.Transform.Rotation.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.Scale.CurrentValueSet += x, x => Layer.Transform.Scale.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + Observable.FromEventPattern(x => Layer.Transform.AnchorPoint.CurrentValueSet += x, x => Layer.Transform.AnchorPoint.CurrentValueSet -= x) + .Subscribe(_ => UpdateTransform()) + .DisposeWith(d); + + _selected = profileEditorService.ProfileElement.Select(p => p == Layer).ToProperty(this, vm => vm.Selected).DisposeWith(d); + + profileEditorService.Time.Subscribe(_ => UpdateTransform()).DisposeWith(d); + Update(); + UpdateTransform(); + }); + } + + public Layer Layer { get; } + public bool Selected => _selected?.Value ?? false; + public Rect LayerBounds => Layer.Bounds.ToRect(); + + public Geometry? ShapeGeometry + { + get => _shapeGeometry; + set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value); + } + + private void Update() + { + if (Layer.General.ShapeType.CurrentValue == LayerShapeType.Rectangle) + ShapeGeometry = new RectangleGeometry(new Rect(0, 0, Layer.Bounds.Width, Layer.Bounds.Height)); + else + ShapeGeometry = new EllipseGeometry(new Rect(0, 0, Layer.Bounds.Width, Layer.Bounds.Height)); + + this.RaisePropertyChanged(nameof(X)); + this.RaisePropertyChanged(nameof(Y)); + this.RaisePropertyChanged(nameof(LayerBounds)); + } + + private void UpdateTransform() + { + if (ShapeGeometry != null) + ShapeGeometry.Transform = new MatrixTransform(Layer.GetTransformMatrix(true, true, true, true).ToMatrix()); + } + + public int X => Layer.Bounds.Left; + public int Y => Layer.Bounds.Top; + public int Order => 2; +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml index ec8a92ae9..6e24c808a 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml @@ -5,19 +5,24 @@ xmlns:visualizers="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerVisualizerView" - x:DataType="visualizers:LayerVisualizerViewModel"> + x:DataType="visualizers:LayerVisualizerViewModel" + ZIndex="1"> + Margin="0 0 2 2" + StrokeDashArray="6,2" + StrokeJoin="Round"> + + + - \ No newline at end of file + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs index d74e985cb..aa5c84eab 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs @@ -2,66 +2,44 @@ using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared; -using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia; using Avalonia.Controls.Mixins; -using Avalonia.Media; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerViewModel { - private Geometry? _shapeGeometry; private ObservableAsPropertyHelper? _selected; public LayerVisualizerViewModel(Layer layer, IProfileEditorService profileEditorService) { Layer = layer; - this.WhenActivated(d => { - Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x).Subscribe(_ => UpdateShape()).DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.Position.CurrentValueSet += x, x => Layer.Transform.Position.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) + Observable.FromEventPattern(x => Layer.RenderPropertiesUpdated += x, x => Layer.RenderPropertiesUpdated -= x) + .Subscribe(_ => Update()) .DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.Rotation.CurrentValueSet += x, x => Layer.Transform.Rotation.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) + _selected = profileEditorService.ProfileElement + .Select(p => p == Layer) + .ToProperty(this, vm => vm.Selected) .DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.Scale.CurrentValueSet += x, x => Layer.Transform.Scale.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) - .DisposeWith(d); - Observable.FromEventPattern(x => Layer.Transform.AnchorPoint.CurrentValueSet += x, x => Layer.Transform.AnchorPoint.CurrentValueSet -= x) - .Subscribe(_ => UpdateTransform()) - .DisposeWith(d); - - _selected = profileEditorService.ProfileElement.Select(p => p == Layer).ToProperty(this, vm => vm.Selected).DisposeWith(d); - - profileEditorService.Time.Subscribe(_ => UpdateTransform()).DisposeWith(d); - UpdateShape(); }); } public Layer Layer { get; } public bool Selected => _selected?.Value ?? false; + public Rect LayerBounds => new(0, 0, Layer.Bounds.Width, Layer.Bounds.Height); - public Geometry? ShapeGeometry + private void Update() { - get => _shapeGeometry; - set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value); + this.RaisePropertyChanged(nameof(X)); + this.RaisePropertyChanged(nameof(Y)); + this.RaisePropertyChanged(nameof(LayerBounds)); } - private void UpdateShape() - { - if (Layer.General.ShapeType.CurrentValue == LayerShapeType.Rectangle) - ShapeGeometry = new RectangleGeometry(Layer.Bounds.ToRect()); - else - ShapeGeometry = new EllipseGeometry(Layer.Bounds.ToRect()); - } - - private void UpdateTransform() - { - if (ShapeGeometry != null) - ShapeGeometry.Transform = new MatrixTransform(Layer.GetTransformMatrix(false, true, true, true).ToMatrix()); - } + public int X => Layer.Bounds.Left; + public int Y => Layer.Bounds.Top; + public int Order => 1; } \ No newline at end of file From bc1b44069c4970d69782efb8ea4f6662808ff89a Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 5 Feb 2022 11:20:51 +0100 Subject: [PATCH 126/270] Profile editor - Added tools and layer visualizers --- src/Artemis.Core/Models/Profile/Layer.cs | 21 ++-- .../ApplicationStateManager.cs | 5 +- .../ProfileEditor/Commands/ChangeLayerLeds.cs | 45 ++++++++ .../ProfileEditor/IProfileEditorService.cs | 5 + .../Services/ProfileEditor/IToolViewModel.cs | 103 ++++++++++++++++++ .../ProfileEditor/ProfileEditorService.cs | 34 ++++-- .../Artemis.UI.Shared/ViewModelBase.cs | 26 +---- .../ApplicationStateManager.cs | 3 + .../Properties/launchSettings.json | 8 ++ src/Avalonia/Artemis.UI/Ninject/UIModule.cs | 10 ++ .../Tabs/DevicePropertiesTabViewModel.cs | 26 ++--- ...uginPrerequisitesInstallDialogViewModel.cs | 22 ++-- ...inPrerequisitesUninstallDialogViewModel.cs | 22 ++-- .../Screens/Plugins/PluginFeatureViewModel.cs | 40 ++++--- .../Plugins/PluginPrerequisiteViewModel.cs | 16 ++- .../Plugins/PluginSettingsViewModel.cs | 27 ++--- .../VisualEditor/Tools/IToolViewModel.cs | 6 - .../Tools/SelectionAddToolView.axaml | 18 +++ .../Tools/SelectionAddToolView.axaml.cs | 24 ++++ .../Tools/SelectionAddToolViewModel.cs | 68 ++++++++++++ .../Tools/SelectionRemoveToolView.axaml | 20 ++++ .../Tools/SelectionRemoveToolView.axaml.cs | 24 ++++ .../Tools/SelectionRemoveToolViewModel.cs | 64 +++++++++++ .../Tools/TransformToolView.axaml | 8 ++ .../Tools/TransformToolView.axaml.cs | 17 +++ .../Tools/TransformToolViewModel.cs | 45 ++++++++ .../VisualEditor/VisualEditorView.axaml | 2 +- .../VisualEditor/VisualEditorViewModel.cs | 16 ++- .../Visualizers/IVisualizerViewModel.cs | 4 +- .../LayerShapeVisualizerView.axaml | 3 +- .../LayerShapeVisualizerViewModel.cs | 67 +++++++++--- .../Visualizers/LayerVisualizerView.axaml | 2 +- .../Visualizers/LayerVisualizerViewModel.cs | 49 +++++++-- .../ProfileEditor/ProfileEditorView.axaml | 39 ++++--- .../ProfileEditor/ProfileEditorViewModel.cs | 27 ++++- .../Services/RegistrationService.cs | 85 ++++++++------- 36 files changed, 776 insertions(+), 225 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json delete mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 8b87d2fc5..3512496f3 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -565,23 +565,24 @@ namespace Artemis.Core OnRenderPropertiesUpdated(); } - internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased) + internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased, SKRect? customBounds = null) { if (Disposed) throw new ObjectDisposedException("Layer"); + SKRect bounds = customBounds ?? Bounds; SKPoint positionProperty = Transform.Position.CurrentValue; // Start at the center of the shape SKPoint position = zeroBased - ? new SKPointI(Bounds.MidX - Bounds.Left, Bounds.MidY - Bounds.Top) - : new SKPointI(Bounds.MidX, Bounds.MidY); + ? new SKPoint(bounds.MidX - bounds.Left, bounds.MidY - Bounds.Top) + : new SKPoint(bounds.MidX, bounds.MidY); // Apply translation if (applyTranslation) { - position.X += positionProperty.X * Bounds.Width; - position.Y += positionProperty.Y * Bounds.Height; + position.X += positionProperty.X * bounds.Width; + position.Y += positionProperty.Y * bounds.Height; } return position; @@ -625,8 +626,9 @@ namespace Artemis.Core /// Whether translation should be included /// Whether the scale should be included /// Whether the rotation should be included + /// Optional custom bounds to base the anchor on /// The transformation matrix containing the current transformation settings - public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation) + public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation, SKRect? customBounds = null) { if (Disposed) throw new ObjectDisposedException("Layer"); @@ -634,15 +636,16 @@ namespace Artemis.Core if (Path == null) return SKMatrix.Empty; + SKRect bounds = customBounds ?? Bounds; SKSize sizeProperty = Transform.Scale.CurrentValue; float rotationProperty = Transform.Rotation.CurrentValue; - SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased); + SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds); SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; // Translation originates from the unscaled center of the shape and is tied to the anchor - float x = anchorPosition.X - (zeroBased ? Bounds.MidX - Bounds.Left : Bounds.MidX) - anchorProperty.X * Bounds.Width; - float y = anchorPosition.Y - (zeroBased ? Bounds.MidY - Bounds.Top : Bounds.MidY) - anchorProperty.Y * Bounds.Height; + float x = anchorPosition.X - (zeroBased ? bounds.MidX - bounds.Left : bounds.MidX) - anchorProperty.X * bounds.Width; + float y = anchorPosition.Y - (zeroBased ? bounds.MidY - bounds.Top : bounds.MidY) - anchorProperty.Y * bounds.Height; SKMatrix transform = SKMatrix.Empty; diff --git a/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs index 146a90751..681063b91 100644 --- a/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs +++ b/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs @@ -4,9 +4,9 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; -using System.Security.Principal; using System.Threading; using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; @@ -36,6 +36,9 @@ namespace Artemis.UI.Linux controlledApplicationLifetime.Exit += (_, _) => { RunForcedShutdownIfEnabled(); + + // Dispose plugins before disposing the kernel because plugins might access services during dispose + kernel.Get().Dispose(); kernel.Dispose(); }; } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs new file mode 100644 index 000000000..a70020101 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to change the LEDs of a layer. +/// +public class ChangeLayerLeds : IProfileEditorCommand +{ + private readonly Layer _layer; + private readonly List _leds; + private readonly List _originalLeds; + + /// + /// Creates a new instance of the class. + /// + public ChangeLayerLeds(Layer layer, List leds) + { + _layer = layer; + _leds = leds; + _originalLeds = new List(_layer.Leds); + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Change layer LEDs"; + + /// + public void Execute() + { + _layer.ClearLeds(); + _layer.AddLeds(_leds); + } + + /// + public void Undo() + { + _layer.ClearLeds(); + _layer.AddLeds(_originalLeds); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 7be4f2843..7165d1800 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -42,6 +42,11 @@ public interface IProfileEditorService : IArtemisSharedUIService /// IObservable PixelsPerSecond { get; } + /// + /// Gets a source list of all available editor tools. + /// + SourceList Tools { get; } + /// /// Connect to the observable list of keyframes and observe any changes starting with the list's initial items. /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs new file mode 100644 index 000000000..5d01eded7 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs @@ -0,0 +1,103 @@ +using System; +using System.Windows.Input; +using Material.Icons; +using ReactiveUI; + +namespace Artemis.UI.Shared.Services.ProfileEditor; + +/// +/// Represents a profile editor tool. +/// +public interface IToolViewModel : IDisposable +{ + /// + /// Gets or sets a boolean indicating whether the tool is selected. + /// + public bool IsSelected { get; set; } + + /// + /// Gets a boolean indicating whether the tool is enabled. + /// + public bool IsEnabled { get; } + + /// + /// Gets a boolean indicating whether or not this tool is exclusive. + /// Exclusive tools deactivate any other exclusive tools when activated. + /// + public bool IsExclusive { get; } + + /// + /// Gets or sets a boolean indicating whether this tool should be shown in the toolbar. + /// + public bool ShowInToolbar { get; } + + /// + /// Gets the order in which this tool should appear in the toolbar. + /// + public int Order { get; } + + /// + /// Gets the icon which this tool should show in the toolbar. + /// + public MaterialIconKind Icon { get; } + + /// + /// Gets the tooltip which this tool should show in the toolbar. + /// + public string ToolTip { get; } +} + +public abstract class ToolViewModel : ActivatableViewModelBase, IToolViewModel +{ + private bool _isSelected; + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #region Implementation of IToolViewModel + + /// + public bool IsSelected + { + get => _isSelected; + set => this.RaiseAndSetIfChanged(ref _isSelected, value); + } + + /// + public abstract bool IsEnabled { get; } + + /// + public abstract bool IsExclusive { get; } + + /// + public abstract bool ShowInToolbar { get; } + + /// + public abstract int Order { get; } + + /// + public abstract MaterialIconKind Icon { get; } + + /// + public abstract string ToolTip { get; } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index 858fb5427..e36a75d43 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -9,6 +10,8 @@ using Artemis.Core.Services; using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.ProfileEditor.Commands; using DynamicData; +using DynamicData.Binding; +using ReactiveUI; using Serilog; namespace Artemis.UI.Shared.Services.ProfileEditor; @@ -43,9 +46,33 @@ internal class ProfileEditorService : IProfileEditorService Playing = _playingSubject.AsObservable(); SuspendedEditing = _suspendedEditingSubject.AsObservable(); PixelsPerSecond = _pixelsPerSecondSubject.AsObservable(); + Tools = new SourceList(); + Tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Subscribe(set => + { + IToolViewModel? changed = set.FirstOrDefault()?.Item.Current; + if (changed == null) + return; + + // Disable all others if the changed one is selected and exclusive + if (changed.IsSelected && changed.IsExclusive) + { + Tools.Edit(list => + { + foreach (IToolViewModel toolViewModel in list.Where(t => t.IsExclusive && t != changed)) + toolViewModel.IsSelected = false; + }); + } + }); } public IObservable SuspendedEditing { get; } + public IObservable ProfileConfiguration { get; } + public IObservable ProfileElement { get; } + public IObservable History { get; } + public IObservable Time { get; } + public IObservable Playing { get; } + public IObservable PixelsPerSecond { get; } + public SourceList Tools { get; } private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration) { @@ -87,13 +114,6 @@ internal class ProfileEditorService : IProfileEditorService } } - public IObservable ProfileConfiguration { get; } - public IObservable ProfileElement { get; } - public IObservable History { get; } - public IObservable Time { get; } - public IObservable Playing { get; } - public IObservable PixelsPerSecond { get; } - public IObservable> ConnectToKeyframes() { return _selectedKeyframes.Connect(); diff --git a/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs index 85926ff55..0d3059328 100644 --- a/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs +++ b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs @@ -84,34 +84,10 @@ namespace Artemis.UI.Shared /// /// Represents the base class for Artemis view models that are interested in the activated event /// - public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable + public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel { - /// - protected ActivatableViewModelBase() - { - this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables)); - } - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - } - /// public ViewModelActivator Activator { get; } = new(); - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } } /// diff --git a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs index aec129e73..a3c1be22d 100644 --- a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs @@ -39,6 +39,9 @@ namespace Artemis.UI.Windows controlledApplicationLifetime.Exit += (_, _) => { RunForcedShutdownIfEnabled(); + + // Dispose plugins before disposing the kernel because plugins might access services during dispose + kernel.Get().Dispose(); kernel.Dispose(); }; } diff --git a/src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json b/src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json new file mode 100644 index 000000000..a1588c6f9 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Artemis.UI.Windows": { + "commandName": "Project", + "commandLineArgs": "--force-elevation --disable-forced-shutdown --pcmr" + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs index af1cfa130..e186b02a9 100644 --- a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs +++ b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs @@ -2,8 +2,10 @@ using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.InstanceProviders; using Artemis.UI.Screens; +using Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia.Platform; using Avalonia.Shared.PlatformSupport; using Ninject.Extensions.Conventions; @@ -39,6 +41,14 @@ namespace Artemis.UI.Ninject .BindAllBaseClasses(); }); + Kernel.Bind(x => + { + x.FromThisAssembly() + .SelectAllClasses() + .InheritedFrom() + .BindAllInterfaces(); + }); + // Bind UI factories Kernel.Bind(x => { diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs index 77edab6ea..02050d9f7 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; @@ -65,8 +66,17 @@ namespace Artemis.UI.Screens.Device this.WhenAnyValue(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling()); - Device.PropertyChanged += DeviceOnPropertyChanged; - _coreService.FrameRendering += OnFrameRendering; + this.WhenActivated(d => + { + Device.PropertyChanged += DeviceOnPropertyChanged; + _coreService.FrameRendering += OnFrameRendering; + + Disposable.Create(() => + { + _coreService.FrameRendering -= OnFrameRendering; + Device.PropertyChanged -= DeviceOnPropertyChanged; + }).DisposeWith(d); + }); } public ArtemisDevice Device { get; } @@ -235,18 +245,6 @@ namespace Artemis.UI.Screens.Device Device.BlueScale = _initialBlueScale; } - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - _coreService.FrameRendering -= OnFrameRendering; - Device.PropertyChanged -= DeviceOnPropertyChanged; - } - - base.Dispose(disposing); - } - private bool GetCategory(DeviceCategory category) { return _categories.Contains(category); diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs index 93d811451..28fb0baff 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; using Artemis.Core; @@ -30,6 +31,15 @@ namespace Artemis.UI.Screens.Plugins CanInstall = false; Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet())); + + this.WhenActivated(d => + { + Disposable.Create(() => + { + _tokenSource?.Cancel(); + _tokenSource?.Dispose(); + }).DisposeWith(d); + }); } public ObservableCollection Prerequisites { get; } @@ -125,17 +135,5 @@ namespace Artemis.UI.Screens.Plugins { return await windowService.ShowDialogAsync(("subjects", subjects)); } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - _tokenSource?.Cancel(); - _tokenSource?.Dispose(); - } - - base.Dispose(disposing); - } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs index 9eeb5c7aa..f489d2726 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; using Artemis.Core; @@ -37,6 +38,15 @@ namespace Artemis.UI.Screens.Plugins // Could be slow so take it off of the UI thread Task.Run(() => CanUninstall = Prerequisites.Any(p => p.PluginPrerequisite.IsMet())); + + this.WhenActivated(d => + { + Disposable.Create(() => + { + _tokenSource?.Cancel(); + _tokenSource?.Dispose(); + }).DisposeWith(d); + }); } public string CancelLabel { get; } @@ -133,17 +143,5 @@ namespace Artemis.UI.Screens.Plugins { return await windowService.ShowDialogAsync(("subjects", subjects), ("cancelLabel", cancelLabel)); } - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - _tokenSource?.Cancel(); - _tokenSource?.Dispose(); - } - - base.Dispose(disposing); - } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs index eaeda45a5..8a6678f3e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; @@ -36,12 +37,25 @@ namespace Artemis.UI.Screens.Plugins FeatureInfo = pluginFeatureInfo; ShowShield = FeatureInfo.Plugin.Info.RequiresAdmin && showShield; - _pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling; - _pluginManagementService.PluginFeatureEnabled += OnFeatureEnableStopped; - _pluginManagementService.PluginFeatureEnableFailed += OnFeatureEnableStopped; + this.WhenActivated(d => + { + _pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling; + _pluginManagementService.PluginFeatureEnabled += OnFeatureEnableStopped; + _pluginManagementService.PluginFeatureEnableFailed += OnFeatureEnableStopped; - FeatureInfo.Plugin.Enabled += PluginOnToggled; - FeatureInfo.Plugin.Disabled += PluginOnToggled; + FeatureInfo.Plugin.Enabled += PluginOnToggled; + FeatureInfo.Plugin.Disabled += PluginOnToggled; + + Disposable.Create(() => + { + _pluginManagementService.PluginFeatureEnabling -= OnFeatureEnabling; + _pluginManagementService.PluginFeatureEnabled -= OnFeatureEnableStopped; + _pluginManagementService.PluginFeatureEnableFailed -= OnFeatureEnableStopped; + + FeatureInfo.Plugin.Enabled -= PluginOnToggled; + FeatureInfo.Plugin.Disabled -= PluginOnToggled; + }).DisposeWith(d); + }); } public PluginFeatureInfo FeatureInfo { get; } @@ -99,22 +113,6 @@ namespace Artemis.UI.Screens.Plugins } } - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - _pluginManagementService.PluginFeatureEnabling -= OnFeatureEnabling; - _pluginManagementService.PluginFeatureEnabled -= OnFeatureEnableStopped; - _pluginManagementService.PluginFeatureEnableFailed -= OnFeatureEnableStopped; - - FeatureInfo.Plugin.Enabled -= PluginOnToggled; - FeatureInfo.Plugin.Disabled -= PluginOnToggled; - } - - base.Dispose(disposing); - } - private async Task UpdateEnabled(bool enable) { if (IsEnabled == enable) diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs index 57b24ee12..85d617e1a 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; using Artemis.Core; @@ -33,7 +34,12 @@ namespace Artemis.UI.Screens.Plugins this.WhenAnyValue(x => x.Installing, x => x.Uninstalling, (i, u) => i || u).ToProperty(this, x => x.Busy, out _busy); this.WhenAnyValue(x => x.ActiveAction, a => Actions.IndexOf(a!)).ToProperty(this, x => x.ActiveStepNumber, out _activeStepNumber); - PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged; + + this.WhenActivated(d => + { + PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged; + Disposable.Create(() => PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged).DisposeWith(d); + }); // Could be slow so take it off of the UI thread Task.Run(() => IsMet = PluginPrerequisite.IsMet()); @@ -105,14 +111,6 @@ namespace Artemis.UI.Screens.Plugins } } - /// - protected override void Dispose(bool disposing) - { - if (disposing) PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged; - - base.Dispose(disposing); - } - private void PluginPrerequisiteOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PluginPrerequisite.CurrentAction)) diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs index a1036f921..2334cfda6 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; @@ -49,12 +50,23 @@ namespace Artemis.UI.Screens.Plugins foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); - _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled; - _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled; + OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null)); InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites)); RemovePrerequisites = ReactiveCommand.CreateFromTask(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites)); + + this.WhenActivated(d => + { + _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled; + _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled; + + Disposable.Create(() => + { + _pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled; + _pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled; + }).DisposeWith(d); + }); } public ReactiveCommand OpenSettings { get; } @@ -237,17 +249,6 @@ namespace Artemis.UI.Screens.Plugins Utilities.OpenUrl(uri.ToString()); } - protected override void Dispose(bool disposing) - { - if (disposing) - { - _pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled; - _pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled; - } - - base.Dispose(disposing); - } - private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e) { this.RaisePropertyChanged(nameof(IsEnabled)); diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs deleted file mode 100644 index fabd8a409..000000000 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools -{ - public interface IToolViewModel - { - } -} diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml new file mode 100644 index 000000000..2e3c5aaae --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs new file mode 100644 index 000000000..0110136ed --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs @@ -0,0 +1,24 @@ +using Artemis.UI.Shared.Events; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Skia; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; + +public class SelectionAddToolView : ReactiveUserControl +{ + public SelectionAddToolView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) + { + ViewModel?.AddLedsInRectangle(e.Rectangle.ToSKRect()); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs new file mode 100644 index 000000000..77710155c --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Avalonia.Controls.Mixins; +using Material.Icons; +using ReactiveUI; +using SkiaSharp; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; + +public class SelectionAddToolViewModel : ToolViewModel +{ + private readonly IProfileEditorService _profileEditorService; + private readonly IRgbService _rgbService; + private readonly ObservableAsPropertyHelper? _isEnabled; + private Layer? _layer; + + /// + public SelectionAddToolViewModel(IProfileEditorService profileEditorService, IRgbService rgbService) + { + _profileEditorService = profileEditorService; + _rgbService = rgbService; + _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); + + this.WhenActivated(d => profileEditorService.ProfileElement.Subscribe(p => _layer = p as Layer).DisposeWith(d)); + } + + /// + public override bool IsEnabled => _isEnabled?.Value ?? false; + + /// + public override bool IsExclusive => true; + + /// + public override bool ShowInToolbar => true; + + /// + public override int Order => 3; + + /// + public override MaterialIconKind Icon => MaterialIconKind.SelectionDrag; + + /// + public override string ToolTip => "Add LEDs to the current layer"; + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + _isEnabled?.Dispose(); + + base.Dispose(disposing); + } + + public void AddLedsInRectangle(SKRect rect) + { + if (_layer == null) + return; + + List leds = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); + _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds)); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml new file mode 100644 index 000000000..0823320a9 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs new file mode 100644 index 000000000..4a618dd3f --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs @@ -0,0 +1,24 @@ +using Artemis.UI.Shared.Events; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Skia; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; + +public class SelectionRemoveToolView : ReactiveUserControl +{ + public SelectionRemoveToolView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) + { + ViewModel?.RemoveLedsInRectangle(e.Rectangle.ToSKRect()); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs new file mode 100644 index 000000000..a36d9ce9c --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using Avalonia.Controls.Mixins; +using Material.Icons; +using ReactiveUI; +using SkiaSharp; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; + +public class SelectionRemoveToolViewModel : ToolViewModel +{ + private readonly ObservableAsPropertyHelper? _isEnabled; + private readonly IProfileEditorService _profileEditorService; + private Layer? _layer; + + /// + public SelectionRemoveToolViewModel(IProfileEditorService profileEditorService) + { + _profileEditorService = profileEditorService; + _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); + this.WhenActivated(d => profileEditorService.ProfileElement.Subscribe(p => _layer = p as Layer).DisposeWith(d)); + } + + /// + public override bool IsEnabled => _isEnabled?.Value ?? false; + + /// + public override bool IsExclusive => true; + + /// + public override bool ShowInToolbar => true; + + /// + public override int Order => 3; + + /// + public override MaterialIconKind Icon => MaterialIconKind.SelectOff; + + /// + public override string ToolTip => "Remove LEDs from the current layer"; + + public void RemoveLedsInRectangle(SKRect rect) + { + if (_layer == null) + return; + + List leds = _layer.Leds.Except(_layer.Leds.Where(l => l.AbsoluteRectangle.IntersectsWith(rect))).ToList(); + _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds)); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + _isEnabled?.Dispose(); + + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml new file mode 100644 index 000000000..d92f01b02 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs new file mode 100644 index 000000000..1149fe2ad --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; + +public class TransformToolView : ReactiveUserControl +{ + public TransformToolView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs new file mode 100644 index 000000000..14ae1e5ec --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs @@ -0,0 +1,45 @@ +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Services.ProfileEditor; +using Material.Icons; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; + +public class TransformToolViewModel : ToolViewModel +{ + private readonly ObservableAsPropertyHelper? _isEnabled; + + /// + public TransformToolViewModel(IProfileEditorService profileEditorService) + { + _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); + } + + /// + public override bool IsEnabled => _isEnabled?.Value ?? false; + + /// + public override bool IsExclusive => true; + + /// + public override bool ShowInToolbar => true; + + /// + public override int Order => 3; + + /// + public override MaterialIconKind Icon => MaterialIconKind.TransitConnectionVariant; + + /// + public override string ToolTip => "Transform the shape of the current layer"; + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + _isEnabled?.Dispose(); + + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml index dfa802ef5..1d7464a62 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml @@ -65,7 +65,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 1ad1fa430..0ed2ce175 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -20,25 +20,26 @@ public class VisualEditorViewModel : ActivatableViewModelBase private readonly IProfileEditorVmFactory _vmFactory; private ObservableAsPropertyHelper? _profileConfiguration; private readonly SourceList _visualizers; + private ReadOnlyObservableCollection _tools; public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory) { _vmFactory = vmFactory; _visualizers = new SourceList(); - - Devices = new ObservableCollection(rgbService.EnabledDevices); - Tools = new ObservableCollection(); - _visualizers.Connect() .Sort(SortExpressionComparer.Ascending(vm => vm.Order)) .Bind(out ReadOnlyObservableCollection visualizers) .Subscribe(); + + Devices = new ObservableCollection(rgbService.EnabledDevices); Visualizers = visualizers; this.WhenActivated(d => { _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); profileEditorService.ProfileConfiguration.Subscribe(CreateVisualizers).DisposeWith(d); + profileEditorService.Tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Filter(t => t.IsSelected).Bind(out ReadOnlyObservableCollection tools).Subscribe().DisposeWith(d); + Tools = tools; }); } @@ -46,7 +47,12 @@ public class VisualEditorViewModel : ActivatableViewModelBase public ObservableCollection Devices { get; } public ReadOnlyObservableCollection Visualizers { get; } - public ObservableCollection Tools { get; } + + public ReadOnlyObservableCollection Tools + { + get => _tools; + set => this.RaiseAndSetIfChanged(ref _tools, value); + } private void CreateVisualizers(ProfileConfiguration? profileConfiguration) { diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs index 385fab60f..c0134b102 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs @@ -2,7 +2,7 @@ public interface IVisualizerViewModel { - int X { get; } - int Y { get; } + double X { get; } + double Y { get; } int Order { get; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml index 0fe4b62bb..4ea1dd32d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml @@ -6,8 +6,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerShapeVisualizerView" x:DataType="visualizers:LayerShapeVisualizerViewModel" - ClipToBounds="False" - ZIndex="2"> + ClipToBounds="False"> + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml index ccd646ccf..adb0fd87f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml @@ -5,7 +5,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Debugger.Performance.PerformanceDebugView"> - + Performance @@ -21,7 +21,19 @@ - + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs index 781ff7e3c..1afef3043 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs @@ -8,16 +8,23 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Screens.Debugger.Performance { public class PerformanceDebugViewModel : ActivatableViewModelBase, IRoutableViewModel { + private readonly ICoreService _coreService; private readonly IPluginManagementService _pluginManagementService; + private double _currentFps; + private string? _renderer; + private int _renderHeight; + private int _renderWidth; - public PerformanceDebugViewModel(IScreen hostScreen, IPluginManagementService pluginManagementService) + public PerformanceDebugViewModel(IScreen hostScreen, ICoreService coreService, IPluginManagementService pluginManagementService) { HostScreen = hostScreen; + _coreService = coreService; _pluginManagementService = pluginManagementService; Timer updateTimer = new(500); @@ -38,6 +45,7 @@ namespace Artemis.UI.Screens.Debugger.Performance .Subscribe(_ => Repopulate()) .DisposeWith(disposables); + HandleActivation(); PopulateItems(); updateTimer.Start(); @@ -45,21 +53,57 @@ namespace Artemis.UI.Screens.Debugger.Performance { updateTimer.Stop(); Items.Clear(); + HandleDeactivation(); }).DisposeWith(disposables); }); } - public ObservableCollection Items { get; } = new(); - + public string UrlPathSegment => "performance"; public IScreen HostScreen { get; } + public ObservableCollection Items { get; } = new(); + + public double CurrentFps + { + get => _currentFps; + set => this.RaiseAndSetIfChanged(ref _currentFps, value); + } + + public int RenderWidth + { + get => _renderWidth; + set => this.RaiseAndSetIfChanged(ref _renderWidth, value); + } + + public int RenderHeight + { + get => _renderHeight; + set => this.RaiseAndSetIfChanged(ref _renderHeight, value); + } + + public string? Renderer + { + get => _renderer; + set => this.RaiseAndSetIfChanged(ref _renderer, value); + } + + private void HandleActivation() + { + Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software"; + _coreService.FrameRendered += CoreServiceOnFrameRendered; + } + + private void HandleDeactivation() + { + _coreService.FrameRendered -= CoreServiceOnFrameRendered; + } private void PopulateItems() { foreach (PerformanceDebugPluginViewModel performanceDebugPluginViewModel in _pluginManagementService.GetAllPlugins() - .Where(p => p.IsEnabled && p.Profilers.Any(pr => pr.Measurements.Any())) - .OrderBy(p => p.Info.Name) - .Select(p => new PerformanceDebugPluginViewModel(p))) + .Where(p => p.IsEnabled && p.Profilers.Any(pr => pr.Measurements.Any())) + .OrderBy(p => p.Info.Name) + .Select(p => new PerformanceDebugPluginViewModel(p))) Items.Add(performanceDebugPluginViewModel); } @@ -69,10 +113,19 @@ namespace Artemis.UI.Screens.Debugger.Performance PopulateItems(); } - private void UpdateTimerOnElapsed(object sender, ElapsedEventArgs e) + private void UpdateTimerOnElapsed(object? sender, ElapsedEventArgs e) { foreach (PerformanceDebugPluginViewModel viewModel in Items) viewModel.Update(); } + + private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) + { + CurrentFps = _coreService.FrameRate; + SKImageInfo bitmapInfo = e.Texture.ImageInfo; + + RenderHeight = bitmapInfo.Height; + RenderWidth = bitmapInfo.Width; + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml index 3143eeecf..e29e77355 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml @@ -24,6 +24,21 @@ - + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs index 6729680c5..51d1bf5a7 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs @@ -102,7 +102,6 @@ namespace Artemis.UI.Screens.Debugger.Render } } - public string UrlPathSegment => "render"; public IScreen HostScreen { get; } } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml index 2e3c5aaae..f6246fcce 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml @@ -9,7 +9,9 @@ + BorderThickness="2" + SelectionFinished="SelectionRectangle_OnSelectionFinished" + ZoomRatio="{Binding $parent[ZoomBorder].ZoomX}"> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs index 0110136ed..b8938f4f5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs @@ -1,4 +1,5 @@ using Artemis.UI.Shared.Events; +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using Avalonia.Skia; @@ -19,6 +20,6 @@ public class SelectionAddToolView : ReactiveUserControl leds = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); - _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds)); + if (inverse) + { + List toRemove = _layer.Leds.Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); + List toAdd = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).Except(toRemove).ToList(); + List leds = _layer.Leds.Except(toRemove).ToList(); + leds.AddRange(toAdd); + + _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds)); + } + else + { + List leds = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList(); + if (expand) + leds.AddRange(_layer.Leds); + _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds.Distinct().ToList())); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml index 0823320a9..69dbdc20e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml @@ -11,7 +11,9 @@ + BorderThickness="2" + SelectionFinished="SelectionRectangle_OnSelectionFinished" + ZoomRatio="{Binding $parent[ZoomBorder].ZoomX}"> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml index 4ea1dd32d..703ec1b30 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml @@ -10,38 +10,53 @@ - - - + - + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs index cbef48437..c2e8936c0 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs @@ -1,3 +1,10 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Controls.Shapes; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -5,14 +12,50 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers { public partial class LayerShapeVisualizerView : ReactiveUserControl { + private ZoomBorder? _zoomBorder; + private readonly Path _layerVisualizerUnbound; + private readonly Path _layerVisualizer; + public LayerShapeVisualizerView() { InitializeComponent(); + _layerVisualizer = this.Get("LayerVisualizer"); + _layerVisualizerUnbound = this.Get("LayerVisualizerUnbound"); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + #region Overrides of TemplatedControl + + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _zoomBorder = (ZoomBorder?) this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); + if (_zoomBorder != null) + _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + base.OnAttachedToLogicalTree(e); + } + + /// + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_zoomBorder != null) + _zoomBorder.PropertyChanged -= ZoomBorderOnPropertyChanged; + base.OnDetachedFromLogicalTree(e); + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null) + return; + + _layerVisualizer.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX); + _layerVisualizerUnbound.StrokeThickness = _layerVisualizer.StrokeThickness; + } + + #endregion } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml index 1ba74c263..b5cc6011c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml @@ -15,14 +15,14 @@ - - + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml.cs index ba1264dea..bb00950f2 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml.cs @@ -1,18 +1,58 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Controls.Shapes; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers { public partial class LayerVisualizerView : ReactiveUserControl { + private ZoomBorder? _zoomBorder; + private readonly Path _layerVisualizer; + public LayerVisualizerView() { InitializeComponent(); + _layerVisualizer = this.Get("LayerVisualizer"); } + #region Overrides of TemplatedControl + + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _zoomBorder = (ZoomBorder?) this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); + if (_zoomBorder != null) + _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + base.OnAttachedToLogicalTree(e); + } + + /// + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_zoomBorder != null) + _zoomBorder.PropertyChanged -= ZoomBorderOnPropertyChanged; + base.OnDetachedFromLogicalTree(e); + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null) + return; + _layerVisualizer.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX); + } + + #endregion + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml index 3ff09a332..3c98dac8b 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml @@ -5,7 +5,7 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Root.DefaultTitleBarView"> - \ No newline at end of file From 12e91b8c812e01bb5132f08b6f164f0879f11c58 Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 8 Feb 2022 23:07:15 +0100 Subject: [PATCH 129/270] Transform tool - WIP commit --- .../DataModelDisplayViewModel.cs | 4 +- .../DefaultDataModelDisplayViewModel.cs | 2 +- .../Extensions/LayerExtensions.cs | 109 ++++++++++ .../ScriptEditorViewModel.cs | 2 +- .../Services/ProfileEditor/IToolViewModel.cs | 2 +- .../Artemis.UI.Shared/ViewModelBase.cs | 198 ++++++++++-------- .../Screens/Debugger/DebugViewModel.cs | 2 +- .../Tabs/DataModel/DataModelDebugViewModel.cs | 10 +- .../PerformanceDebugMeasurementViewModel.cs | 10 +- .../Performance/PerformanceDebugViewModel.cs | 8 +- .../Tabs/Render/RenderDebugViewModel.cs | 10 +- .../Screens/Device/DeviceSettingsViewModel.cs | 2 +- .../Device/Tabs/DeviceLedsTabViewModel.cs | 2 +- .../Tabs/DevicePropertiesTabViewModel.cs | 18 +- .../Device/Tabs/InputMappingsTabViewModel.cs | 2 +- ...uginPrerequisitesInstallDialogViewModel.cs | 12 +- ...inPrerequisitesUninstallDialogViewModel.cs | 6 +- .../Screens/Plugins/PluginFeatureViewModel.cs | 2 +- .../Plugins/PluginPrerequisiteViewModel.cs | 8 +- .../Plugins/PluginSettingsViewModel.cs | 10 +- .../Panels/MenuBar/MenuBarViewModel.cs | 2 +- .../Panels/Playback/PlaybackViewModel.cs | 6 +- .../ProfileTree/ProfileTreeViewModel.cs | 2 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 8 +- .../Properties/PropertyGroupViewModel.cs | 6 +- .../Panels/Properties/PropertyViewModel.cs | 6 +- .../Keyframes/TimelineKeyframeViewModel.cs | 4 +- .../Timeline/TimelineGroupViewModel.cs | 2 +- .../Panels/StatusBar/StatusBarViewModel.cs | 4 +- .../Tools/TransformToolView.axaml | 157 +++++++++++++- .../Tools/TransformToolView.axaml.cs | 144 +++++++++++++ .../Tools/TransformToolViewModel.cs | 126 ++++++++++- .../VisualEditor/VisualEditorViewModel.cs | 2 +- .../LayerShapeVisualizerViewModel.cs | 8 +- .../Visualizers/LayerVisualizerViewModel.cs | 6 +- .../ProfileEditor/ProfileEditorViewModel.cs | 2 +- .../Artemis.UI/Screens/Root/RootViewModel.cs | 4 +- .../Screens/Root/SplashViewModel.cs | 2 +- .../Settings/Tabs/AboutTabViewModel.cs | 10 +- .../Settings/Tabs/PluginsTabViewModel.cs | 2 +- .../ProfileConfigurationEditViewModel.cs | 22 +- .../Sidebar/SidebarCategoryViewModel.cs | 2 +- .../Screens/Sidebar/SidebarViewModel.cs | 4 +- .../SurfaceEditor/ListDeviceViewModel.cs | 4 +- .../SurfaceEditor/SurfaceDeviceViewModel.cs | 4 +- .../Screens/Workshop/WorkshopView.axaml | 19 +- .../Screens/Workshop/WorkshopViewModel.cs | 13 ++ 47 files changed, 782 insertions(+), 208 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs index 49003ab86..6dd1d3ded 100644 --- a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs @@ -22,7 +22,7 @@ namespace Artemis.UI.Shared.DataModelVisualization set { if (Equals(value, _displayValue)) return; - this.RaiseAndSetIfChanged(ref _displayValue, value); + RaiseAndSetIfChanged(ref _displayValue, value); OnDisplayValueUpdated(); } } @@ -56,7 +56,7 @@ namespace Artemis.UI.Shared.DataModelVisualization public DataModelPropertyAttribute? PropertyDescription { get => _propertyDescription; - internal set => this.RaiseAndSetIfChanged(ref _propertyDescription, value); + internal set => RaiseAndSetIfChanged(ref _propertyDescription, value); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs b/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs index 80578c565..40a265190 100644 --- a/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs @@ -28,7 +28,7 @@ namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display public string Display { get => _display; - set => this.RaiseAndSetIfChanged(ref _display, value); + set => RaiseAndSetIfChanged(ref _display, value); } protected override void OnDisplayValueUpdated() diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs new file mode 100644 index 000000000..2de45c517 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs @@ -0,0 +1,109 @@ +using System.Linq; +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.UI.Shared.Extensions; + +/// +/// Provides utilities when working with layers in UI elements. +/// +public static class LayerExtensions +{ + /// + /// Returns the layer's bounds in real coordinates. + /// + public static SKRect GetLayerBounds(this Layer layer) + { + return new SKRect( + layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.X), + layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.Y), + layer.Leds.Max(l => l.RgbLed.AbsoluteBoundary.Location.X + l.RgbLed.AbsoluteBoundary.Size.Width), + layer.Leds.Max(l => l.RgbLed.AbsoluteBoundary.Location.Y + l.RgbLed.AbsoluteBoundary.Size.Height) + ); + } + + /// + /// Returns the layer's anchor in real coordinates. + /// + public static SKPoint GetLayerAnchorPosition(this Layer layer, SKPoint? positionOverride = null) + { + SKRect layerBounds = GetLayerBounds(layer); + SKPoint positionProperty = layer.Transform.Position.CurrentValue; + if (positionOverride != null) + positionProperty = positionOverride.Value; + + // Start at the center of the shape + SKPoint position = new(layerBounds.MidX, layerBounds.MidY); + + // Apply translation + position.X += positionProperty.X * layerBounds.Width; + position.Y += positionProperty.Y * layerBounds.Height; + + return position; + } + + /// + /// Returns an absolute and scaled rectangular path for the given layer in real coordinates. + /// + public static SKPath GetLayerPath(this Layer layer, bool includeTranslation, bool includeScale, bool includeRotation, SKPoint? anchorOverride = null) + { + SKRect layerBounds = GetLayerBounds(layer); + + // Apply transformation like done by the core during layer rendering (same differences apply as in GetLayerTransformGroup) + SKPoint anchorPosition = GetLayerAnchorPosition(layer); + if (anchorOverride != null) + anchorPosition = anchorOverride.Value; + + SKPoint anchorProperty = layer.Transform.AnchorPoint.CurrentValue; + + // Translation originates from the unscaled center of the shape and is tied to the anchor + float x = anchorPosition.X - layerBounds.MidX - anchorProperty.X * layerBounds.Width; + float y = anchorPosition.Y - layerBounds.MidY - anchorProperty.Y * layerBounds.Height; + + SKPath path = new(); + path.AddRect(layerBounds); + if (includeTranslation) + path.Transform(SKMatrix.CreateTranslation(x, y)); + if (includeScale) + path.Transform(SKMatrix.CreateScale(layer.Transform.Scale.CurrentValue.Width / 100f, layer.Transform.Scale.CurrentValue.Height / 100f, anchorPosition.X, anchorPosition.Y)); + if (includeRotation) + path.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue, anchorPosition.X, anchorPosition.Y)); + + return path; + } + + /// + /// Returns a new point normalized to 0.0-1.0 + /// + public static SKPoint GetScaledPoint(this Layer layer, SKPoint point, bool absolute) + { + SKRect bounds = GetLayerBounds(layer); + if (absolute) + return new SKPoint( + 100 / bounds.Width * (point.X - bounds.Left) / 100, + 100 / bounds.Height * (point.Y - bounds.Top) / 100 + ); + + return new SKPoint( + 100 / bounds.Width * point.X / 100, + 100 / bounds.Height * point.Y / 100 + ); + } + + /// + /// Returns the offset from the given point to the top-left of the layer + /// + public static SKPoint GetDragOffset(this Layer layer, SKPoint dragStart) + { + // Figure out what the top left will be if the shape moves to the current cursor position + SKPoint scaledDragStart = GetScaledPoint(layer, dragStart, true); + SKPoint tempAnchor = GetLayerAnchorPosition(layer, scaledDragStart); + SKPoint tempTopLeft = GetLayerPath(layer, true, true, true, tempAnchor)[0]; + + // Get the shape's position + SKPoint topLeft = GetLayerPath(layer, true, true, true)[0]; + + // The difference between the two is the offset + return topLeft - tempTopLeft; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs index 65e1fc9b1..416d2614a 100644 --- a/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs @@ -42,7 +42,7 @@ namespace Artemis.UI.Shared.ScriptingProviders public Script? Script { get => _script; - internal set => this.RaiseAndSetIfChanged(ref _script, value); + internal set => RaiseAndSetIfChanged(ref _script, value); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs index 5d01eded7..bb841721c 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs @@ -78,7 +78,7 @@ public abstract class ToolViewModel : ActivatableViewModelBase, IToolViewModel public bool IsSelected { get => _isSelected; - set => this.RaiseAndSetIfChanged(ref _isSelected, value); + set => RaiseAndSetIfChanged(ref _isSelected, value); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs index 0d3059328..e89b30258 100644 --- a/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs +++ b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs @@ -1,118 +1,142 @@ using System; -using System.Reactive.Disposables; +using System.Collections.Generic; +using System.Runtime.CompilerServices; using Artemis.UI.Shared.Events; using FluentAvalonia.UI.Controls; +using JetBrains.Annotations; using ReactiveUI; using ReactiveUI.Validation.Helpers; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared; + +/// +/// Represents the base class for Artemis view models +/// +public abstract class ContentDialogViewModelBase : ReactiveValidationObject, IActivatableViewModel, IDisposable { /// - /// Represents the base class for Artemis view models + /// Gets the content dialog that hosts the view model /// - public abstract class ContentDialogViewModelBase : ReactiveValidationObject, IActivatableViewModel, IDisposable + public ContentDialog? ContentDialog { get; internal set; } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) { - /// - /// Gets the content dialog that hosts the view model - /// - public ContentDialog? ContentDialog { get; internal set; } + } - #region Implementation of IActivatableViewModel + #region Implementation of IActivatableViewModel - /// - public ViewModelActivator Activator { get; } = new(); + /// + public ViewModelActivator Activator { get; } = new(); - #endregion + #endregion - #region IDisposable + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - } +/// +/// Represents the base class for Artemis view models +/// +public abstract class ViewModelValidationBase : ReactiveValidationObject +{ + private string? _displayName; - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + /// + /// Gets or sets the display name of the view model + /// + public string? DisplayName + { + get => _displayName; + set => this.RaiseAndSetIfChanged(ref _displayName, value); + } +} - #endregion +/// +/// Represents the base class for Artemis view models +/// +public abstract class ViewModelBase : ReactiveObject +{ + private string? _displayName; + + /// + /// Gets or sets the display name of the view model + /// + public string? DisplayName + { + get => _displayName; + set => RaiseAndSetIfChanged(ref _displayName, value); } /// - /// Represents the base class for Artemis view models + /// RaiseAndSetIfChanged fully implements a Setter for a read-write property on a ReactiveObject, using + /// CallerMemberName to raise the notification and the ref to the backing field to set the property. /// - public abstract class ViewModelValidationBase : ReactiveValidationObject + /// The type of the return value. + /// A Reference to the backing field for this property. + /// The new value. + /// + /// The name of the property, usually automatically provided through the CallerMemberName + /// attribute. + /// + /// The newly set value, normally discarded. + [NotifyPropertyChangedInvocator] + public TRet RaiseAndSetIfChanged(ref TRet backingField, TRet newValue, [CallerMemberName] string? propertyName = null) { - private string? _displayName; + if (propertyName is null) + throw new ArgumentNullException(nameof(propertyName)); - /// - /// Gets or sets the display name of the view model - /// - public string? DisplayName - { - get => _displayName; - set => this.RaiseAndSetIfChanged(ref _displayName, value); - } + if (EqualityComparer.Default.Equals(backingField, newValue)) + return newValue; + + this.RaisePropertyChanging(propertyName); + backingField = newValue; + this.RaisePropertyChanged(propertyName); + return newValue; + } +} + +/// +/// Represents the base class for Artemis view models that are interested in the activated event +/// +public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel +{ + /// + public ViewModelActivator Activator { get; } = new(); +} + +/// +/// Represents the base class for Artemis view models used to drive dialogs +/// +public abstract class DialogViewModelBase : ActivatableViewModelBase +{ + /// + /// Closes the dialog with the given + /// + /// The result of the dialog + public void Close(TResult result) + { + CloseRequested?.Invoke(this, new DialogClosedEventArgs(result)); } /// - /// Represents the base class for Artemis view models + /// Closes the dialog without a result /// - public abstract class ViewModelBase : ReactiveObject + public void Cancel() { - private string? _displayName; - - /// - /// Gets or sets the display name of the view model - /// - public string? DisplayName - { - get => _displayName; - set => this.RaiseAndSetIfChanged(ref _displayName, value); - } + CancelRequested?.Invoke(this, EventArgs.Empty); } - /// - /// Represents the base class for Artemis view models that are interested in the activated event - /// - public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel - { - /// - public ViewModelActivator Activator { get; } = new(); - } - - /// - /// Represents the base class for Artemis view models used to drive dialogs - /// - public abstract class DialogViewModelBase : ActivatableViewModelBase - { - /// - /// Closes the dialog with the given - /// - /// The result of the dialog - public void Close(TResult result) - { - CloseRequested?.Invoke(this, new DialogClosedEventArgs(result)); - } - - /// - /// Closes the dialog without a result - /// - public void Cancel() - { - CancelRequested?.Invoke(this, EventArgs.Empty); - } - - internal event EventHandler>? CloseRequested; - internal event EventHandler? CancelRequested; - } + internal event EventHandler>? CloseRequested; + internal event EventHandler? CancelRequested; } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs index d1b773f1a..f6bda99aa 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugViewModel.cs @@ -38,7 +38,7 @@ namespace Artemis.UI.Screens.Debugger public NavigationViewItem? SelectedItem { get => _selectedItem; - set => this.RaiseAndSetIfChanged(ref _selectedItem, value); + set => RaiseAndSetIfChanged(ref _selectedItem, value); } private void NavigateToSelectedItem(NavigationViewItem item) diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs index 7c9fbf74b..9c62836b8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/DataModel/DataModelDebugViewModel.cs @@ -59,13 +59,13 @@ namespace Artemis.UI.Screens.Debugger.DataModel public DataModelPropertiesViewModel? MainDataModel { get => _mainDataModel; - set => this.RaiseAndSetIfChanged(ref _mainDataModel, value); + set => RaiseAndSetIfChanged(ref _mainDataModel, value); } public string? PropertySearch { get => _propertySearch; - set => this.RaiseAndSetIfChanged(ref _propertySearch, value); + set => RaiseAndSetIfChanged(ref _propertySearch, value); } public bool SlowUpdates @@ -73,7 +73,7 @@ namespace Artemis.UI.Screens.Debugger.DataModel get => _slowUpdates; set { - this.RaiseAndSetIfChanged(ref _slowUpdates, value); + RaiseAndSetIfChanged(ref _slowUpdates, value); _updateTimer.Interval = _slowUpdates ? 500 : 25; } } @@ -85,7 +85,7 @@ namespace Artemis.UI.Screens.Debugger.DataModel get => _selectedModule; set { - this.RaiseAndSetIfChanged(ref _selectedModule, value); + RaiseAndSetIfChanged(ref _selectedModule, value); GetDataModel(); } } @@ -95,7 +95,7 @@ namespace Artemis.UI.Screens.Debugger.DataModel get => _isModuleFilterEnabled; set { - this.RaiseAndSetIfChanged(ref _isModuleFilterEnabled, value); + RaiseAndSetIfChanged(ref _isModuleFilterEnabled, value); if (!IsModuleFilterEnabled) SelectedModule = null; diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs index 4c1391c5d..37f6470fb 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugMeasurementViewModel.cs @@ -22,31 +22,31 @@ namespace Artemis.UI.Screens.Debugger.Performance public string? Last { get => _last; - set => this.RaiseAndSetIfChanged(ref _last, value); + set => RaiseAndSetIfChanged(ref _last, value); } public string? Average { get => _average; - set => this.RaiseAndSetIfChanged(ref _average, value); + set => RaiseAndSetIfChanged(ref _average, value); } public string? Min { get => _min; - set => this.RaiseAndSetIfChanged(ref _min, value); + set => RaiseAndSetIfChanged(ref _min, value); } public string? Max { get => _max; - set => this.RaiseAndSetIfChanged(ref _max, value); + set => RaiseAndSetIfChanged(ref _max, value); } public string? Percentile { get => _percentile; - set => this.RaiseAndSetIfChanged(ref _percentile, value); + set => RaiseAndSetIfChanged(ref _percentile, value); } public void Update() diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs index 1afef3043..d4e81011b 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugViewModel.cs @@ -66,25 +66,25 @@ namespace Artemis.UI.Screens.Debugger.Performance public double CurrentFps { get => _currentFps; - set => this.RaiseAndSetIfChanged(ref _currentFps, value); + set => RaiseAndSetIfChanged(ref _currentFps, value); } public int RenderWidth { get => _renderWidth; - set => this.RaiseAndSetIfChanged(ref _renderWidth, value); + set => RaiseAndSetIfChanged(ref _renderWidth, value); } public int RenderHeight { get => _renderHeight; - set => this.RaiseAndSetIfChanged(ref _renderHeight, value); + set => RaiseAndSetIfChanged(ref _renderHeight, value); } public string? Renderer { get => _renderer; - set => this.RaiseAndSetIfChanged(ref _renderer, value); + set => RaiseAndSetIfChanged(ref _renderer, value); } private void HandleActivation() diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs index 51d1bf5a7..92fe7e804 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugViewModel.cs @@ -35,31 +35,31 @@ namespace Artemis.UI.Screens.Debugger.Render public Bitmap? CurrentFrame { get => _currentFrame; - set => this.RaiseAndSetIfChanged(ref _currentFrame, value); + set => RaiseAndSetIfChanged(ref _currentFrame, value); } public double CurrentFps { get => _currentFps; - set => this.RaiseAndSetIfChanged(ref _currentFps, value); + set => RaiseAndSetIfChanged(ref _currentFps, value); } public int RenderWidth { get => _renderWidth; - set => this.RaiseAndSetIfChanged(ref _renderWidth, value); + set => RaiseAndSetIfChanged(ref _renderWidth, value); } public int RenderHeight { get => _renderHeight; - set => this.RaiseAndSetIfChanged(ref _renderHeight, value); + set => RaiseAndSetIfChanged(ref _renderHeight, value); } public string? Renderer { get => _renderer; - set => this.RaiseAndSetIfChanged(ref _renderer, value); + set => RaiseAndSetIfChanged(ref _renderer, value); } private void HandleActivation() diff --git a/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs index 315e3744f..00d04d167 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/DeviceSettingsViewModel.cs @@ -57,7 +57,7 @@ namespace Artemis.UI.Screens.Device public bool TogglingDevice { get => _togglingDevice; - set => this.RaiseAndSetIfChanged(ref _togglingDevice, value); + set => RaiseAndSetIfChanged(ref _togglingDevice, value); } public void IdentifyDevice() diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs index f11989e93..bb30a4238 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DeviceLedsTabViewModel.cs @@ -60,7 +60,7 @@ namespace Artemis.UI.Screens.Device get => _isSelected; set { - if (!this.RaiseAndSetIfChanged(ref _isSelected, value)) + if (!RaiseAndSetIfChanged(ref _isSelected, value)) return; Apply(); } diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs index 02050d9f7..aefb65d4f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs @@ -84,55 +84,55 @@ namespace Artemis.UI.Screens.Device public int X { get => _x; - set => this.RaiseAndSetIfChanged(ref _x, value); + set => RaiseAndSetIfChanged(ref _x, value); } public int Y { get => _y; - set => this.RaiseAndSetIfChanged(ref _y, value); + set => RaiseAndSetIfChanged(ref _y, value); } public float Scale { get => _scale; - set => this.RaiseAndSetIfChanged(ref _scale, value); + set => RaiseAndSetIfChanged(ref _scale, value); } public int Rotation { get => _rotation; - set => this.RaiseAndSetIfChanged(ref _rotation, value); + set => RaiseAndSetIfChanged(ref _rotation, value); } public float RedScale { get => _redScale; - set => this.RaiseAndSetIfChanged(ref _redScale, value); + set => RaiseAndSetIfChanged(ref _redScale, value); } public float GreenScale { get => _greenScale; - set => this.RaiseAndSetIfChanged(ref _greenScale, value); + set => RaiseAndSetIfChanged(ref _greenScale, value); } public float BlueScale { get => _blueScale; - set => this.RaiseAndSetIfChanged(ref _blueScale, value); + set => RaiseAndSetIfChanged(ref _blueScale, value); } public SKColor CurrentColor { get => _currentColor; - set => this.RaiseAndSetIfChanged(ref _currentColor, value); + set => RaiseAndSetIfChanged(ref _currentColor, value); } public bool DisplayOnDevices { get => _displayOnDevices; - set => this.RaiseAndSetIfChanged(ref _displayOnDevices, value); + set => RaiseAndSetIfChanged(ref _displayOnDevices, value); } // This solution won't scale well but I don't expect there to be many more categories. diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs index 6a174e615..e7ba90cda 100644 --- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/InputMappingsTabViewModel.cs @@ -37,7 +37,7 @@ namespace Artemis.UI.Screens.Device public ArtemisLed? SelectedLed { get => _selectedLed; - set => this.RaiseAndSetIfChanged(ref _selectedLed, value); + set => RaiseAndSetIfChanged(ref _selectedLed, value); } public ObservableCollection<(ArtemisLed, ArtemisLed)> InputMappings { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs index 28fb0baff..c5240786f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs @@ -47,37 +47,37 @@ namespace Artemis.UI.Screens.Plugins public PluginPrerequisiteViewModel? ActivePrerequisite { get => _activePrerequisite; - set => this.RaiseAndSetIfChanged(ref _activePrerequisite, value); + set => RaiseAndSetIfChanged(ref _activePrerequisite, value); } public bool ShowProgress { get => _showProgress; - set => this.RaiseAndSetIfChanged(ref _showProgress, value); + set => RaiseAndSetIfChanged(ref _showProgress, value); } public bool ShowIntro { get => _showIntro; - set => this.RaiseAndSetIfChanged(ref _showIntro, value); + set => RaiseAndSetIfChanged(ref _showIntro, value); } public bool ShowFailed { get => _showFailed; - set => this.RaiseAndSetIfChanged(ref _showFailed, value); + set => RaiseAndSetIfChanged(ref _showFailed, value); } public bool ShowInstall { get => _showInstall; - set => this.RaiseAndSetIfChanged(ref _showInstall, value); + set => RaiseAndSetIfChanged(ref _showInstall, value); } public bool CanInstall { get => _canInstall; - set => this.RaiseAndSetIfChanged(ref _canInstall, value); + set => RaiseAndSetIfChanged(ref _canInstall, value); } public async Task Install() diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs index f489d2726..16323dac4 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs @@ -55,19 +55,19 @@ namespace Artemis.UI.Screens.Plugins public PluginPrerequisiteViewModel? ActivePrerequisite { get => _activePrerequisite; - set => this.RaiseAndSetIfChanged(ref _activePrerequisite, value); + set => RaiseAndSetIfChanged(ref _activePrerequisite, value); } public bool CanUninstall { get => _canUninstall; - set => this.RaiseAndSetIfChanged(ref _canUninstall, value); + set => RaiseAndSetIfChanged(ref _canUninstall, value); } public bool IsFinished { get => _isFinished; - set => this.RaiseAndSetIfChanged(ref _isFinished, value); + set => RaiseAndSetIfChanged(ref _isFinished, value); } public async Task Uninstall() diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs index 8a6678f3e..4e5a72da3 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs @@ -66,7 +66,7 @@ namespace Artemis.UI.Screens.Plugins public bool Enabling { get => _enabling; - set => this.RaiseAndSetIfChanged(ref _enabling, value); + set => RaiseAndSetIfChanged(ref _enabling, value); } public bool IsEnabled diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs index 85d617e1a..7fc226fc8 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs @@ -50,7 +50,7 @@ namespace Artemis.UI.Screens.Plugins public PluginPrerequisiteActionViewModel? ActiveAction { get => _activeAction; - set => this.RaiseAndSetIfChanged(ref _activeAction, value); + set => RaiseAndSetIfChanged(ref _activeAction, value); } public PluginPrerequisite PluginPrerequisite { get; } @@ -58,19 +58,19 @@ namespace Artemis.UI.Screens.Plugins public bool Installing { get => _installing; - set => this.RaiseAndSetIfChanged(ref _installing, value); + set => RaiseAndSetIfChanged(ref _installing, value); } public bool Uninstalling { get => _uninstalling; - set => this.RaiseAndSetIfChanged(ref _uninstalling, value); + set => RaiseAndSetIfChanged(ref _uninstalling, value); } public bool IsMet { get => _isMet; - set => this.RaiseAndSetIfChanged(ref _isMet, value); + set => RaiseAndSetIfChanged(ref _isMet, value); } public bool Busy => _busy.Value; diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs index 2334cfda6..6898bc122 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs @@ -78,13 +78,13 @@ namespace Artemis.UI.Screens.Plugins public Plugin Plugin { get => _plugin; - set => this.RaiseAndSetIfChanged(ref _plugin, value); + set => RaiseAndSetIfChanged(ref _plugin, value); } public bool Enabling { get => _enabling; - set => this.RaiseAndSetIfChanged(ref _enabling, value); + set => RaiseAndSetIfChanged(ref _enabling, value); } public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; @@ -100,7 +100,7 @@ namespace Artemis.UI.Screens.Plugins get => _isSettingsPopupOpen; set { - if (!this.RaiseAndSetIfChanged(ref _isSettingsPopupOpen, value)) return; + if (!RaiseAndSetIfChanged(ref _isSettingsPopupOpen, value)) return; CheckPrerequisites(); } } @@ -108,13 +108,13 @@ namespace Artemis.UI.Screens.Plugins public bool CanInstallPrerequisites { get => _canInstallPrerequisites; - set => this.RaiseAndSetIfChanged(ref _canInstallPrerequisites, value); + set => RaiseAndSetIfChanged(ref _canInstallPrerequisites, value); } public bool CanRemovePrerequisites { get => _canRemovePrerequisites; - set => this.RaiseAndSetIfChanged(ref _canRemovePrerequisites, value); + set => RaiseAndSetIfChanged(ref _canRemovePrerequisites, value); } private void ExecuteOpenSettings() diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index de0dd23f3..df9021b49 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -18,6 +18,6 @@ public class MenuBarViewModel : ActivatableViewModelBase public ProfileEditorHistory? History { get => _history; - set => this.RaiseAndSetIfChanged(ref _history, value); + set => RaiseAndSetIfChanged(ref _history, value); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs index 25936d2a8..a07d4aa6f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs @@ -50,19 +50,19 @@ public class PlaybackViewModel : ActivatableViewModelBase public bool Repeating { get => _repeating; - set => this.RaiseAndSetIfChanged(ref _repeating, value); + set => RaiseAndSetIfChanged(ref _repeating, value); } public bool RepeatTimeline { get => _repeatTimeline; - set => this.RaiseAndSetIfChanged(ref _repeatTimeline, value); + set => RaiseAndSetIfChanged(ref _repeatTimeline, value); } public bool RepeatSegment { get => _repeatSegment; - set => this.RaiseAndSetIfChanged(ref _repeatSegment, value); + set => RaiseAndSetIfChanged(ref _repeatSegment, value); } public void PlayFromStart() diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 7cec467b0..fa6ffe96d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -48,7 +48,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public TreeItemViewModel? SelectedChild { get => _selectedChild; - set => this.RaiseAndSetIfChanged(ref _selectedChild, value); + set => RaiseAndSetIfChanged(ref _selectedChild, value); } private void SelectCurrentProfileElement(RenderProfileElement? element) diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index b24e33450..86ecb14c6 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -89,19 +89,19 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public ProfileElement? ProfileElement { get => _profileElement; - set => this.RaiseAndSetIfChanged(ref _profileElement, value); + set => RaiseAndSetIfChanged(ref _profileElement, value); } public bool IsExpanded { get => _isExpanded; - set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public bool Renaming { get => _renaming; - set => this.RaiseAndSetIfChanged(ref _renaming, value); + set => RaiseAndSetIfChanged(ref _renaming, value); } public TreeItemViewModel? Parent { get; set; } @@ -118,7 +118,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public string? RenameValue { get => _renameValue; - set => this.RaiseAndSetIfChanged(ref _renameValue, value); + set => RaiseAndSetIfChanged(ref _renameValue, value); } public async Task ShowBrokenStateExceptions() diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs index eb453dc1f..202d5f4df 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs @@ -64,19 +64,19 @@ public class PropertyGroupViewModel : ViewModelBase public bool IsVisible { get => _isVisible; - set => this.RaiseAndSetIfChanged(ref _isVisible, value); + set => RaiseAndSetIfChanged(ref _isVisible, value); } public bool IsExpanded { get => _isExpanded; - set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + set => RaiseAndSetIfChanged(ref _isExpanded, value); } public bool HasChildren { get => _hasChildren; - set => this.RaiseAndSetIfChanged(ref _hasChildren, value); + set => RaiseAndSetIfChanged(ref _hasChildren, value); } public List GetAllKeyframeViewModels(bool expandedOnly) diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs index 3f6e00d25..36cbbd55f 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs @@ -30,18 +30,18 @@ public class PropertyViewModel : ViewModelBase public bool IsVisible { get => _isVisible; - set => this.RaiseAndSetIfChanged(ref _isVisible, value); + set => RaiseAndSetIfChanged(ref _isVisible, value); } public bool IsHighlighted { get => _isHighlighted; - set => this.RaiseAndSetIfChanged(ref _isHighlighted, value); + set => RaiseAndSetIfChanged(ref _isHighlighted, value); } public bool IsExpanded { get => _isExpanded; - set => this.RaiseAndSetIfChanged(ref _isExpanded, value); + set => RaiseAndSetIfChanged(ref _isExpanded, value); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs index be3aa871d..38bbf7940 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/Keyframes/TimelineKeyframeViewModel.cs @@ -47,13 +47,13 @@ public class TimelineKeyframeViewModel : ActivatableViewModelBase, ITimelineK public double X { get => _x; - set => this.RaiseAndSetIfChanged(ref _x, value); + set => RaiseAndSetIfChanged(ref _x, value); } public string Timestamp { get => _timestamp; - set => this.RaiseAndSetIfChanged(ref _timestamp, value); + set => RaiseAndSetIfChanged(ref _timestamp, value); } public bool IsSelected => _isSelected?.Value ?? false; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs index a78dace60..87cac2175 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Timeline/TimelineGroupViewModel.cs @@ -36,7 +36,7 @@ public class TimelineGroupViewModel : ActivatableViewModelBase public ObservableCollection? KeyframePositions { get => _keyframePositions; - set => this.RaiseAndSetIfChanged(ref _keyframePositions, value); + set => RaiseAndSetIfChanged(ref _keyframePositions, value); } private void UpdateKeyframePositions() diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs index ea8d621ab..e409816dd 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs @@ -52,12 +52,12 @@ public class StatusBarViewModel : ActivatableViewModelBase public string? StatusMessage { get => _statusMessage; - set => this.RaiseAndSetIfChanged(ref _statusMessage, value); + set => RaiseAndSetIfChanged(ref _statusMessage, value); } public bool ShowStatusMessage { get => _showStatusMessage; - set => this.RaiseAndSetIfChanged(ref _showStatusMessage, value); + set => RaiseAndSetIfChanged(ref _showStatusMessage, value); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml index d92f01b02..f0b7c4bdf 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml @@ -2,7 +2,160 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:tools="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools.TransformToolView"> - Welcome to Avalonia! + x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools.TransformToolView" + x:DataType="tools:TransformToolViewModel"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs index 1149fe2ad..67562ca14 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs @@ -1,10 +1,20 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolView : ReactiveUserControl { + private ZoomBorder? _zoomBorder; + private PointerPoint _dragOffset; + public TransformToolView() { InitializeComponent(); @@ -14,4 +24,138 @@ public class TransformToolView : ReactiveUserControl { AvaloniaXamlLoader.Load(this); } + + #region Zoom + + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _zoomBorder = (ZoomBorder?)this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); + if (_zoomBorder != null) + _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + base.OnAttachedToLogicalTree(e); + } + + /// + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_zoomBorder != null) + _zoomBorder.PropertyChanged -= ZoomBorderOnPropertyChanged; + base.OnDetachedFromLogicalTree(e); + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null) + return; + + // TODO + } + + #endregion + + #region Rotation + + private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + _dragOffset = e.GetCurrentPoint(_zoomBorder); + + e.Pointer.Capture(element); + e.Handled = true; + } + + private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Pointer.Capture(null); + e.Handled = true; + } + + private void RotationOnPointerMoved(object? sender, PointerEventArgs e) + { + IInputElement? element = (IInputElement?) sender; + if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Handled = true; + } + + #endregion + + #region Movement + + private void MoveOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + _dragOffset = e.GetCurrentPoint(_zoomBorder); + + e.Pointer.Capture(element); + e.Handled = true; + } + + private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Pointer.Capture(null); + e.Handled = true; + } + + private void MoveOnPointerMoved(object? sender, PointerEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Handled = true; + } + + #endregion + + #region Resizing + + private void ResizeOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + _dragOffset = e.GetCurrentPoint(_zoomBorder); + + e.Pointer.Capture(element); + e.Handled = true; + } + + private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Pointer.Capture(null); + e.Handled = true; + } + + private void ResizeOnPointerMoved(object? sender, PointerEventArgs e) + { + IInputElement? element = (IInputElement?)sender; + if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + e.Handled = true; + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs index 14ae1e5ec..e1b53e1e9 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs @@ -1,21 +1,80 @@ -using System.Reactive.Linq; +using System; +using System.Reactive; +using System.Reactive.Linq; using Artemis.Core; +using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; +using Avalonia; +using Avalonia.Controls.Mixins; using Material.Icons; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolViewModel : ToolViewModel { - private readonly ObservableAsPropertyHelper? _isEnabled; + private readonly ObservableAsPropertyHelper _isEnabled; + private RelativePoint _relativeAnchor; + private double _inverseRotation; + private ObservableAsPropertyHelper? _layer; + private double _rotation; + private Rect _shapeBounds; + private Point _anchor; /// public TransformToolViewModel(IProfileEditorService profileEditorService) { + // Not disposed when deactivated but when really disposed _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); + + this.WhenActivated(d => + { + _layer = profileEditorService.ProfileElement.Select(p => p as Layer).ToProperty(this, vm => vm.Layer).DisposeWith(d); + + this.WhenAnyValue(vm => vm.Layer) + .Select(l => l != null + ? Observable.FromEventPattern(x => l.RenderPropertiesUpdated += x, x => l.RenderPropertiesUpdated -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => Update()) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.Layer) + .Select(l => l != null + ? Observable.FromEventPattern(x => l.Transform.Position.CurrentValueSet += x, x => l.Transform.Position.CurrentValueSet -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => Update()) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.Layer) + .Select(l => l != null + ? Observable.FromEventPattern(x => l.Transform.Rotation.CurrentValueSet += x, x => l.Transform.Rotation.CurrentValueSet -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => Update()) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.Layer) + .Select(l => l != null + ? Observable.FromEventPattern(x => l.Transform.Scale.CurrentValueSet += x, x => l.Transform.Scale.CurrentValueSet -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => Update()) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.Layer) + .Select(l => l != null + ? Observable.FromEventPattern(x => l.Transform.AnchorPoint.CurrentValueSet += x, x => l.Transform.AnchorPoint.CurrentValueSet -= x) + : Observable.Never>()) + .Switch() + .Subscribe(_ => Update()) + .DisposeWith(d); + + this.WhenAnyValue(vm => vm.Layer).Subscribe(_ => Update()).DisposeWith(d); + profileEditorService.Time.Subscribe(_ => Update()).DisposeWith(d); + }); } + public Layer? Layer => _layer?.Value; + /// public override bool IsEnabled => _isEnabled?.Value ?? false; @@ -34,6 +93,36 @@ public class TransformToolViewModel : ToolViewModel /// public override string ToolTip => "Transform the shape of the current layer"; + public Rect ShapeBounds + { + get => _shapeBounds; + set => RaiseAndSetIfChanged(ref _shapeBounds, value); + } + + public double Rotation + { + get => _rotation; + set => RaiseAndSetIfChanged(ref _rotation, value); + } + + public double InverseRotation + { + get => _inverseRotation; + set => RaiseAndSetIfChanged(ref _inverseRotation, value); + } + + public RelativePoint RelativeAnchor + { + get => _relativeAnchor; + set => RaiseAndSetIfChanged(ref _relativeAnchor, value); + } + + public Point Anchor + { + get => _anchor; + set => RaiseAndSetIfChanged(ref _anchor, value); + } + /// protected override void Dispose(bool disposing) { @@ -42,4 +131,37 @@ public class TransformToolViewModel : ToolViewModel base.Dispose(disposing); } + + private void Update() + { + if (Layer == null) + return; + + SKPath path = new(); + path.AddRect(Layer.GetLayerBounds()); + path.Transform(Layer.GetTransformMatrix(false, true, true, false)); + + ShapeBounds = path.Bounds.ToRect(); + Rotation = Layer.Transform.Rotation.CurrentValue; + InverseRotation = Rotation * -1; + + // The middle of the element is 0.5/0.5 in Avalonia, in Artemis it's 0.0/0.0 so compensate for that below + SKPoint layerAnchor = Layer.Transform.AnchorPoint.CurrentValue; + RelativeAnchor = new RelativePoint(layerAnchor.X + 0.5, layerAnchor.Y + 0.5, RelativeUnit.Relative); + Anchor = new Point(ShapeBounds.Width * RelativeAnchor.Point.X, ShapeBounds.Height * RelativeAnchor.Point.Y); + } + + public enum ShapeControlPoint + { + TopLeft, + TopRight, + BottomRight, + BottomLeft, + TopCenter, + RightCenter, + BottomCenter, + LeftCenter, + LayerShape, + Anchor + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 0ed2ce175..43ca7c63c 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -51,7 +51,7 @@ public class VisualEditorViewModel : ActivatableViewModelBase public ReadOnlyObservableCollection Tools { get => _tools; - set => this.RaiseAndSetIfChanged(ref _tools, value); + set => RaiseAndSetIfChanged(ref _tools, value); } private void CreateVisualizers(ProfileConfiguration? profileConfiguration) diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs index 24fc77b95..c180f00da 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs @@ -54,25 +54,25 @@ public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualiz public Rect LayerBounds { get => _layerBounds; - private set => this.RaiseAndSetIfChanged(ref _layerBounds, value); + private set => RaiseAndSetIfChanged(ref _layerBounds, value); } public double X { get => _x; - set => this.RaiseAndSetIfChanged(ref _x, value); + set => RaiseAndSetIfChanged(ref _x, value); } public double Y { get => _y; - set => this.RaiseAndSetIfChanged(ref _y, value); + set => RaiseAndSetIfChanged(ref _y, value); } public Geometry? ShapeGeometry { get => _shapeGeometry; - set => this.RaiseAndSetIfChanged(ref _shapeGeometry, value); + set => RaiseAndSetIfChanged(ref _shapeGeometry, value); } public int Order => 2; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs index 2750e4a8d..32fd84f96 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs @@ -40,19 +40,19 @@ public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerVie public Rect LayerBounds { get => _layerBounds; - private set => this.RaiseAndSetIfChanged(ref _layerBounds, value); + private set => RaiseAndSetIfChanged(ref _layerBounds, value); } public double X { get => _x; - set => this.RaiseAndSetIfChanged(ref _x, value); + set => RaiseAndSetIfChanged(ref _x, value); } public double Y { get => _y; - set => this.RaiseAndSetIfChanged(ref _y, value); + set => RaiseAndSetIfChanged(ref _y, value); } public int Order => 1; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 03b4aeaa0..a28bf40de 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -70,7 +70,7 @@ namespace Artemis.UI.Screens.ProfileEditor public ReadOnlyObservableCollection Tools { get => _tools; - set => this.RaiseAndSetIfChanged(ref _tools, value); + set => RaiseAndSetIfChanged(ref _tools, value); } public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs index e88ca311d..59acf2659 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootViewModel.cs @@ -77,13 +77,13 @@ namespace Artemis.UI.Screens.Root public SidebarViewModel? SidebarViewModel { get => _sidebarViewModel; - set => this.RaiseAndSetIfChanged(ref _sidebarViewModel, value); + set => RaiseAndSetIfChanged(ref _sidebarViewModel, value); } public ViewModelBase? TitleBarViewModel { get => _titleBarViewModel; - set => this.RaiseAndSetIfChanged(ref _titleBarViewModel, value); + set => RaiseAndSetIfChanged(ref _titleBarViewModel, value); } private void CurrentMainWindowOnClosed(object? sender, EventArgs e) diff --git a/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs index e12ab63d6..d4f21eb52 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/SplashViewModel.cs @@ -30,7 +30,7 @@ namespace Artemis.UI.Screens.Root public string Status { get => _status; - set => this.RaiseAndSetIfChanged(ref _status, value); + set => RaiseAndSetIfChanged(ref _status, value); } private void OnPluginManagementServiceOnPluginManagementLoaded(object? sender, PluginEventArgs args) diff --git a/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/AboutTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/AboutTabViewModel.cs index c0641a06d..666bb5686 100644 --- a/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/AboutTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/AboutTabViewModel.cs @@ -27,31 +27,31 @@ namespace Artemis.UI.Screens.Settings public string? Version { get => _version; - set => this.RaiseAndSetIfChanged(ref _version, value); + set => RaiseAndSetIfChanged(ref _version, value); } public Bitmap? RobertProfileImage { get => _robertProfileImage; - set => this.RaiseAndSetIfChanged(ref _robertProfileImage, value); + set => RaiseAndSetIfChanged(ref _robertProfileImage, value); } public Bitmap? DarthAffeProfileImage { get => _darthAffeProfileImage; - set => this.RaiseAndSetIfChanged(ref _darthAffeProfileImage, value); + set => RaiseAndSetIfChanged(ref _darthAffeProfileImage, value); } public Bitmap? DrMeteorProfileImage { get => _drMeteorProfileImage; - set => this.RaiseAndSetIfChanged(ref _drMeteorProfileImage, value); + set => RaiseAndSetIfChanged(ref _drMeteorProfileImage, value); } public Bitmap? KaiProfileImage { get => _kaiProfileImage; - set => this.RaiseAndSetIfChanged(ref _kaiProfileImage, value); + set => RaiseAndSetIfChanged(ref _kaiProfileImage, value); } private async Task Activate() diff --git a/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs index 2777a26e5..39d3e3073 100644 --- a/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs @@ -46,7 +46,7 @@ namespace Artemis.UI.Screens.Settings public string? SearchPluginInput { get => _searchPluginInput; - set => this.RaiseAndSetIfChanged(ref _searchPluginInput, value); + set => RaiseAndSetIfChanged(ref _searchPluginInput, value); } public void OpenUrl(string url) diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index 06a005cea..366a3e90f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -64,31 +64,31 @@ namespace Artemis.UI.Screens.Sidebar public ProfileConfiguration ProfileConfiguration { get => _profileConfiguration; - set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value); + set => RaiseAndSetIfChanged(ref _profileConfiguration, value); } public string ProfileName { get => _profileName; - set => this.RaiseAndSetIfChanged(ref _profileName, value); + set => RaiseAndSetIfChanged(ref _profileName, value); } public ProfileConfigurationHotkeyMode HotkeyMode { get => _hotkeyMode; - set => this.RaiseAndSetIfChanged(ref _hotkeyMode, value); + set => RaiseAndSetIfChanged(ref _hotkeyMode, value); } public Hotkey? EnableHotkey { get => _enableHotkey; - set => this.RaiseAndSetIfChanged(ref _enableHotkey, value); + set => RaiseAndSetIfChanged(ref _enableHotkey, value); } public Hotkey? DisableHotkey { get => _disableHotkey; - set => this.RaiseAndSetIfChanged(ref _disableHotkey, value); + set => RaiseAndSetIfChanged(ref _disableHotkey, value); } public ObservableCollection Modules { get; } @@ -96,7 +96,7 @@ namespace Artemis.UI.Screens.Sidebar public ProfileModuleViewModel? SelectedModule { get => _selectedModule; - set => this.RaiseAndSetIfChanged(ref _selectedModule, value); + set => RaiseAndSetIfChanged(ref _selectedModule, value); } public async Task Import() @@ -183,31 +183,31 @@ namespace Artemis.UI.Screens.Sidebar public ProfileConfigurationIconType IconType { get => _iconType; - set => this.RaiseAndSetIfChanged(ref _iconType, value); + set => RaiseAndSetIfChanged(ref _iconType, value); } public ObservableCollection? MaterialIcons { get => _materialIcons; - set => this.RaiseAndSetIfChanged(ref _materialIcons, value); + set => RaiseAndSetIfChanged(ref _materialIcons, value); } public ProfileIconViewModel? SelectedMaterialIcon { get => _selectedMaterialIcon; - set => this.RaiseAndSetIfChanged(ref _selectedMaterialIcon, value); + set => RaiseAndSetIfChanged(ref _selectedMaterialIcon, value); } public Bitmap? SelectedBitmapSource { get => _selectedBitmapSource; - set => this.RaiseAndSetIfChanged(ref _selectedBitmapSource, value); + set => RaiseAndSetIfChanged(ref _selectedBitmapSource, value); } public SvgImage? SelectedSvgSource { get => _selectedSvgSource; - set => this.RaiseAndSetIfChanged(ref _selectedSvgSource, value); + set => RaiseAndSetIfChanged(ref _selectedSvgSource, value); } private void LoadIcon() diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs index cc87bfda5..244b45660 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs @@ -53,7 +53,7 @@ namespace Artemis.UI.Screens.Sidebar public SidebarProfileConfigurationViewModel? SelectedProfileConfiguration { get => _selectedProfileConfiguration; - set => this.RaiseAndSetIfChanged(ref _selectedProfileConfiguration, value); + set => RaiseAndSetIfChanged(ref _selectedProfileConfiguration, value); } public bool ShowItems diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index aa72a7b5e..8d788f49c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -89,13 +89,13 @@ namespace Artemis.UI.Screens.Sidebar public ArtemisDevice? HeaderDevice { get => _headerDevice; - set => this.RaiseAndSetIfChanged(ref _headerDevice, value); + set => RaiseAndSetIfChanged(ref _headerDevice, value); } public SidebarScreenViewModel? SelectedSidebarScreen { get => _selectedSidebarScreen; - set => this.RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); + set => RaiseAndSetIfChanged(ref _selectedSidebarScreen, value); } public SidebarCategoryViewModel AddProfileCategoryViewModel(ProfileCategory profileCategory) diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs index 947198734..c9f5db87a 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/ListDeviceViewModel.cs @@ -20,13 +20,13 @@ namespace Artemis.UI.Screens.SurfaceEditor public bool IsSelected { get => _isSelected; - set => this.RaiseAndSetIfChanged(ref _isSelected, value); + set => RaiseAndSetIfChanged(ref _isSelected, value); } public SKColor Color { get => _color; - set => this.RaiseAndSetIfChanged(ref _color, value); + set => RaiseAndSetIfChanged(ref _color, value); } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs index 7800ebf5c..5dcf03e62 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs @@ -53,7 +53,7 @@ namespace Artemis.UI.Screens.SurfaceEditor get => _selectionStatus; set { - this.RaiseAndSetIfChanged(ref _selectionStatus, value); + RaiseAndSetIfChanged(ref _selectionStatus, value); this.RaisePropertyChanged(nameof(Highlighted)); } } @@ -65,7 +65,7 @@ namespace Artemis.UI.Screens.SurfaceEditor public Cursor Cursor { get => _cursor; - set => this.RaiseAndSetIfChanged(ref _cursor, value); + set => RaiseAndSetIfChanged(ref _cursor, value); } public void StartMouseDrag(Point mouseStartPosition) diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml index 2a4589ba1..e3a16ed34 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -6,24 +6,26 @@ xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared" + xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Workshop.WorkshopView"> + x:Class="Artemis.UI.Screens.Workshop.WorkshopView" + x:DataType="workshop:WorkshopViewModel"> Workshop!! :3 Notification tests - - - - @@ -36,6 +38,13 @@ + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 35b280461..95778a15f 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -1,6 +1,8 @@ using System.Reactive; +using System.Reactive.Linq; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; +using Avalonia.Input; using ReactiveUI; namespace Artemis.UI.Screens.Workshop @@ -8,10 +10,13 @@ namespace Artemis.UI.Screens.Workshop public class WorkshopViewModel : MainScreenViewModel { private readonly INotificationService _notificationService; + private StandardCursorType _selectedCursor; + private readonly ObservableAsPropertyHelper _cursor; public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop") { _notificationService = notificationService; + _cursor = this.WhenAnyValue(vm => vm.SelectedCursor).Select(c => new Cursor(c)).ToProperty(this, vm => vm.Cursor); DisplayName = "Workshop"; ShowNotification = ReactiveCommand.Create(ExecuteShowNotification); @@ -19,6 +24,14 @@ namespace Artemis.UI.Screens.Workshop public ReactiveCommand ShowNotification { get; set; } + public StandardCursorType SelectedCursor + { + get => _selectedCursor; + set => RaiseAndSetIfChanged(ref _selectedCursor, value); + } + + public Cursor Cursor => _cursor.Value; + private void ExecuteShowNotification(NotificationSeverity severity) { _notificationService.CreateNotification().WithTitle("Test title").WithMessage("Test message").WithSeverity(severity).Show(); From 1716eba8ec5d2eef1dc87a908311ff417d536311 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 10 Feb 2022 23:38:27 +0100 Subject: [PATCH 130/270] Transform tool - Mostly implemented resizing --- .../Extensions/LayerExtensions.cs | 48 ++--- .../Tools/TransformToolView.axaml.cs | 140 +++++++++++--- .../Tools/TransformToolViewModel.cs | 177 ++++++++++++++++-- 3 files changed, 302 insertions(+), 63 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs index 2de45c517..a1d2cd5d9 100644 --- a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs @@ -45,30 +45,14 @@ public static class LayerExtensions /// /// Returns an absolute and scaled rectangular path for the given layer in real coordinates. /// - public static SKPath GetLayerPath(this Layer layer, bool includeTranslation, bool includeScale, bool includeRotation, SKPoint? anchorOverride = null) + public static SKPath GetLayerPath(this Layer layer, bool includeTranslation, bool includeScale, bool includeRotation) { SKRect layerBounds = GetLayerBounds(layer); - // Apply transformation like done by the core during layer rendering (same differences apply as in GetLayerTransformGroup) - SKPoint anchorPosition = GetLayerAnchorPosition(layer); - if (anchorOverride != null) - anchorPosition = anchorOverride.Value; - - SKPoint anchorProperty = layer.Transform.AnchorPoint.CurrentValue; - - // Translation originates from the unscaled center of the shape and is tied to the anchor - float x = anchorPosition.X - layerBounds.MidX - anchorProperty.X * layerBounds.Width; - float y = anchorPosition.Y - layerBounds.MidY - anchorProperty.Y * layerBounds.Height; - + SKMatrix transform = layer.GetTransformMatrix(false, includeTranslation, includeScale, includeRotation, layerBounds); SKPath path = new(); path.AddRect(layerBounds); - if (includeTranslation) - path.Transform(SKMatrix.CreateTranslation(x, y)); - if (includeScale) - path.Transform(SKMatrix.CreateScale(layer.Transform.Scale.CurrentValue.Width / 100f, layer.Transform.Scale.CurrentValue.Height / 100f, anchorPosition.X, anchorPosition.Y)); - if (includeRotation) - path.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue, anchorPosition.X, anchorPosition.Y)); - + path.Transform(transform); return path; } @@ -91,19 +75,27 @@ public static class LayerExtensions } /// - /// Returns the offset from the given point to the top-left of the layer + /// Returns the offset from the given point to the closest sides of the layer's shape bounds /// public static SKPoint GetDragOffset(this Layer layer, SKPoint dragStart) { - // Figure out what the top left will be if the shape moves to the current cursor position - SKPoint scaledDragStart = GetScaledPoint(layer, dragStart, true); - SKPoint tempAnchor = GetLayerAnchorPosition(layer, scaledDragStart); - SKPoint tempTopLeft = GetLayerPath(layer, true, true, true, tempAnchor)[0]; + SKRect bounds = layer.GetLayerPath(true, true, false).Bounds; + SKPoint anchor = layer.GetLayerAnchorPosition(); - // Get the shape's position - SKPoint topLeft = GetLayerPath(layer, true, true, true)[0]; + float xOffset = 0f, yOffset = 0f; - // The difference between the two is the offset - return topLeft - tempTopLeft; + // X offset + if (dragStart.X < anchor.X) + xOffset = bounds.Left - dragStart.X; + else if (dragStart.X > anchor.X) + xOffset = bounds.Right - dragStart.X; + + // Y offset + if (dragStart.Y < anchor.Y) + yOffset = bounds.Top - dragStart.Y; + else if (dragStart.Y > anchor.Y) + yOffset = bounds.Bottom - dragStart.Y; + + return new SKPoint(xOffset, yOffset); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs index 67562ca14..cf78734c3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs @@ -1,23 +1,63 @@ using System; using System.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Extensions; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; +using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Skia; +using Avalonia.Styling; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolView : ReactiveUserControl { private ZoomBorder? _zoomBorder; - private PointerPoint _dragOffset; + private SKPoint _dragStart; + private SKPoint _dragOffset; + + private readonly Ellipse _rotateTopLeft; + private readonly Ellipse _rotateTopRight; + private readonly Ellipse _rotateBottomRight; + private readonly Ellipse _rotateBottomLeft; + + private readonly Rectangle _resizeTopCenter; + private readonly Rectangle _resizeRightCenter; + private readonly Rectangle _resizeBottomCenter; + private readonly Rectangle _resizeLeftCenter; + private readonly Rectangle _resizeTopLeft; + private readonly Rectangle _resizeTopRight; + private readonly Rectangle _resizeBottomRight; + private readonly Rectangle _resizeBottomLeft; + + private readonly Ellipse _anchorPoint; public TransformToolView() { InitializeComponent(); + + _rotateTopLeft = this.Get("RotateTopLeft"); + _rotateTopRight = this.Get("RotateTopRight"); + _rotateBottomRight = this.Get("RotateBottomRight"); + _rotateBottomLeft = this.Get("RotateBottomLeft"); + + _resizeTopCenter = this.Get("ResizeTopCenter"); + _resizeRightCenter = this.Get("ResizeRightCenter"); + _resizeBottomCenter = this.Get("ResizeBottomCenter"); + _resizeLeftCenter = this.Get("ResizeLeftCenter"); + _resizeTopLeft = this.Get("ResizeTopLeft"); + _resizeTopRight = this.Get("ResizeTopRight"); + _resizeBottomRight = this.Get("ResizeBottomRight"); + _resizeBottomLeft = this.Get("ResizeBottomLeft"); + + _anchorPoint = this.Get("AnchorPoint"); } private void InitializeComponent() @@ -30,7 +70,7 @@ public class TransformToolView : ReactiveUserControl /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - _zoomBorder = (ZoomBorder?)this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); + _zoomBorder = (ZoomBorder?) this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); if (_zoomBorder != null) _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; base.OnAttachedToLogicalTree(e); @@ -58,11 +98,12 @@ public class TransformToolView : ReactiveUserControl private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragOffset = e.GetCurrentPoint(_zoomBorder); + _dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint(); + _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); e.Pointer.Capture(element); e.Handled = true; @@ -70,8 +111,8 @@ public class TransformToolView : ReactiveUserControl private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Pointer.Capture(null); @@ -80,8 +121,8 @@ public class TransformToolView : ReactiveUserControl private void RotationOnPointerMoved(object? sender, PointerEventArgs e) { - IInputElement? element = (IInputElement?) sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Handled = true; @@ -93,11 +134,12 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerPressed(object? sender, PointerPressedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragOffset = e.GetCurrentPoint(_zoomBorder); + _dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint(); + _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); e.Pointer.Capture(element); e.Handled = true; @@ -105,8 +147,8 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Pointer.Capture(null); @@ -115,8 +157,8 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerMoved(object? sender, PointerEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; e.Handled = true; @@ -128,11 +170,15 @@ public class TransformToolView : ReactiveUserControl private void ResizeOnPointerPressed(object? sender, PointerPressedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragOffset = e.GetCurrentPoint(_zoomBorder); + _dragStart = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); + _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); + + SKPoint position = GetPositionForViewModel(e); + ViewModel.StartResize(position); e.Pointer.Capture(element); e.Handled = true; @@ -140,22 +186,70 @@ public class TransformToolView : ReactiveUserControl private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null) return; + SKPoint position = GetPositionForViewModel(e); + ViewModel.FinishResize(position, GetResizeDirection(element), e.KeyModifiers.HasFlag(KeyModifiers.Shift)); + e.Pointer.Capture(null); e.Handled = true; } private void ResizeOnPointerMoved(object? sender, PointerEventArgs e) { - IInputElement? element = (IInputElement?)sender; - if (element == null || e.Pointer.Captured != element || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + Shape? element = (Shape?) sender; + if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; + SKPoint position = GetPositionForViewModel(e); + ViewModel.UpdateResize(position, GetResizeDirection(element), e.KeyModifiers.HasFlag(KeyModifiers.Shift)); + e.Handled = true; } #endregion + + private SKPoint GetPositionForViewModel(PointerEventArgs e) + { + if (ViewModel?.Layer == null) + return SKPoint.Empty; + + SKPoint point = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); + return point + _dragOffset; + } + + private static SKPoint CounteractLayerRotation(SKPoint point, Layer layer) + { + SKPoint pivot = layer.GetLayerAnchorPosition(); + + using SKPath counterRotatePath = new(); + counterRotatePath.AddPoly(new[] {SKPoint.Empty, point}, false); + counterRotatePath.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y)); + + return counterRotatePath.Points[1]; + } + + private TransformToolViewModel.ResizeSide GetResizeDirection(Shape shape) + { + if (ReferenceEquals(shape, _resizeTopLeft)) + return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Left; + if (ReferenceEquals(shape, _resizeTopRight)) + return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(shape, _resizeBottomRight)) + return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(shape, _resizeBottomLeft)) + return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Left; + if (ReferenceEquals(shape, _resizeTopCenter)) + return TransformToolViewModel.ResizeSide.Top; + if (ReferenceEquals(shape, _resizeRightCenter)) + return TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(shape, _resizeBottomCenter)) + return TransformToolViewModel.ResizeSide.Bottom; + if (ReferenceEquals(shape, _resizeLeftCenter)) + return TransformToolViewModel.ResizeSide.Left; + + throw new ArgumentException("Given shape isn't a resize shape"); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs index e1b53e1e9..d78c51f2b 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs @@ -1,9 +1,12 @@ using System; +using System.Diagnostics; +using System.Linq; using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; using Material.Icons; @@ -14,6 +17,7 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolViewModel : ToolViewModel { + private readonly IProfileEditorService _profileEditorService; private readonly ObservableAsPropertyHelper _isEnabled; private RelativePoint _relativeAnchor; private double _inverseRotation; @@ -21,10 +25,13 @@ public class TransformToolViewModel : ToolViewModel private double _rotation; private Rect _shapeBounds; private Point _anchor; + private TimeSpan _time; /// public TransformToolViewModel(IProfileEditorService profileEditorService) { + _profileEditorService = profileEditorService; + // Not disposed when deactivated but when really disposed _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled); @@ -69,7 +76,11 @@ public class TransformToolViewModel : ToolViewModel .DisposeWith(d); this.WhenAnyValue(vm => vm.Layer).Subscribe(_ => Update()).DisposeWith(d); - profileEditorService.Time.Subscribe(_ => Update()).DisposeWith(d); + profileEditorService.Time.Subscribe(t => + { + _time = t; + Update(); + }).DisposeWith(d); }); } @@ -151,17 +162,159 @@ public class TransformToolViewModel : ToolViewModel Anchor = new Point(ShapeBounds.Width * RelativeAnchor.Point.X, ShapeBounds.Height * RelativeAnchor.Point.Y); } - public enum ShapeControlPoint + #region Resizing + + private SKSize _resizeStartScale; + private bool _hadKeyframe; + + public void StartResize(SKPoint position) { - TopLeft, - TopRight, - BottomRight, - BottomLeft, - TopCenter, - RightCenter, - BottomCenter, - LeftCenter, - LayerShape, - Anchor + if (Layer == null) + return; + + _resizeStartScale = Layer.Transform.Scale.CurrentValue; + _hadKeyframe = Layer.Transform.Scale.Keyframes.Any(k => k.Position == _time); + } + + public void FinishResize(SKPoint position, ResizeSide side, bool evenSides) + { + if (Layer == null) + return; + + // Grab the size one last time + SKSize size = UpdateResize(position, side, evenSides); + + // If the layer has keyframes, new keyframes may have been added while the user was dragging + if (Layer.Transform.Scale.KeyframesEnabled) + { + // If there was already a keyframe at the old spot, edit that keyframe + if (_hadKeyframe) + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _resizeStartScale, _time)); + // If there was no keyframe yet, remove the keyframe that was created while dragging and create a permanent one + else + { + Layer.Transform.Scale.RemoveKeyframe(Layer.Transform.Scale.Keyframes.First(k => k.Position == _time)); + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _time)); + } + } + else + { + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _resizeStartScale, _time)); + } + } + + public SKSize UpdateResize(SKPoint position, ResizeSide side, bool evenSides) + { + if (Layer == null) + return SKSize.Empty; + + SKPoint normalizedAnchor = Layer.Transform.AnchorPoint; + // TODO Remove when anchor is centralized at 0.5,0.5 + normalizedAnchor = new SKPoint(normalizedAnchor.X + 0.5f, normalizedAnchor.Y + 0.5f); + + // The anchor is used to ensure a side can't shrink past the anchor + SKPoint anchor = Layer.GetLayerAnchorPosition(); + // The bounds are used to determine whether to shrink or grow + SKRect shapeBounds = Layer.GetLayerPath(true, true, false).Bounds; + + float width = shapeBounds.Width; + float height = shapeBounds.Height; + + // Resize each side as requested, the sides of each axis are mutually exclusive + if (side.HasFlag(ResizeSide.Left)) + { + if (position.X > anchor.X) + position.X = anchor.X; + + float anchorOffset = 1f - normalizedAnchor.X; + float difference = MathF.Abs(shapeBounds.Left - position.X); + if (position.X < shapeBounds.Left) + width += difference / anchorOffset; + else + width -= difference / anchorOffset; + } + else if (side.HasFlag(ResizeSide.Right)) + { + if (position.X < anchor.X) + position.X = anchor.X; + + float anchorOffset = normalizedAnchor.X; + float difference = MathF.Abs(shapeBounds.Right - position.X); + if (position.X > shapeBounds.Right) + width += difference / anchorOffset; + else + width -= difference / anchorOffset; + } + + if (side.HasFlag(ResizeSide.Top)) + { + if (position.Y > anchor.Y) + position.Y = anchor.Y; + + float anchorOffset = 1f - normalizedAnchor.Y; + float difference = MathF.Abs(shapeBounds.Top - position.Y); + if (position.Y < shapeBounds.Top) + height += difference / anchorOffset; + else + height -= difference / anchorOffset; + } + else if (side.HasFlag(ResizeSide.Bottom)) + { + if (position.Y < anchor.Y) + position.Y = anchor.Y; + + float anchorOffset = normalizedAnchor.Y; + float difference = MathF.Abs(shapeBounds.Bottom - position.Y); + if (position.Y > shapeBounds.Bottom) + height += difference / anchorOffset; + else + height -= difference / anchorOffset; + } + + // Even out the sides to the size of the longest side + if (evenSides) + { + if (width > height) + width = height; + else + height = width; + } + + // Normalize the scale to a percentage + SKRect bounds = Layer.GetLayerBounds(); + width = width / bounds.Width * 100f; + height = height / bounds.Height * 100f; + + Layer.Transform.Scale.SetCurrentValue(new SKSize(width, height), _time); + return new SKSize(width, height); + } + + #endregion + + #region Rotating + + + + #endregion + + #region Movement + + + + #endregion + + #region Anchor movement + + + + #endregion + + [Flags] + public enum ResizeSide + { + Top = 1, + Right = 2, + Bottom = 4, + Left = 8, } } \ No newline at end of file From 32ebf5f00092c662c4cc081360643d0299084880 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 13 Feb 2022 21:19:32 +0100 Subject: [PATCH 131/270] Transform tool - Finished initial implementation --- src/Artemis.Core/Constants.cs | 2 +- src/Artemis.Core/Models/Profile/Layer.cs | 12 +- .../Profile/LayerProperties/LayerProperty.cs | 17 +- .../LayerProperties/LayerPropertyPreview.cs | 93 ++++ .../Profile/LayerTransformProperties.cs | 6 +- .../Visualization/Tools/EditToolViewModel.cs | 5 +- .../Extensions/LayerExtensions.cs | 9 +- .../Providers/ICursorProvider.cs | 24 + .../Commands/ResetLayerProperty.cs | 52 ++ .../Commands/UpdateLayerProperty.cs | 15 +- src/Avalonia/Artemis.UI.Windows/App.axaml.cs | 5 +- .../ApplicationStateManager.cs | 2 +- .../Artemis.UI.Windows.csproj | 13 + .../Assets/Cursors/aero_crosshair.png | Bin 0 -> 347 bytes .../Assets/Cursors/aero_crosshair_minus.png | Bin 0 -> 356 bytes .../Assets/Cursors/aero_crosshair_plus.png | Bin 0 -> 369 bytes .../Assets/Cursors/aero_drag.png | Bin 0 -> 627 bytes .../Assets/Cursors/aero_drag_horizontal.png | Bin 0 -> 1124 bytes .../Assets/Cursors/aero_pen_min.png | Bin 0 -> 460 bytes .../Assets/Cursors/aero_pen_plus.png | Bin 0 -> 472 bytes .../Assets/Cursors/aero_rotate.png | Bin 0 -> 825 bytes .../Assets/Cursors/aero_rotate_bl.png | Bin 0 -> 673 bytes .../Assets/Cursors/aero_rotate_br.png | Bin 0 -> 678 bytes .../Assets/Cursors/aero_rotate_tl.png | Bin 0 -> 671 bytes .../Assets/Cursors/aero_rotate_tr.png | Bin 0 -> 669 bytes .../Ninject/WindowsModule.cs | 18 + .../Providers/CursorProvider.cs | 22 + .../Artemis.UI/ArtemisBootstrapper.cs | 4 +- .../Properties/Tree/TreePropertyViewModel.cs | 13 +- .../Panels/StatusBar/StatusBarViewModel.cs | 15 +- .../Tools/TransformToolView.axaml | 294 ++++++----- .../Tools/TransformToolView.axaml.cs | 444 ++++++++++------ .../Tools/TransformToolViewModel.cs | 474 ++++++++++++------ .../LayerShapeVisualizerViewModel.cs | 14 +- .../Visualizers/LayerVisualizerViewModel.cs | 21 +- .../Services/RegistrationService.cs | 19 +- 36 files changed, 1094 insertions(+), 499 deletions(-) create mode 100644 src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_minus.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_plus.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_plus.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_bl.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_br.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tl.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tr.png create mode 100644 src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs create mode 100644 src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index b63d493c2..2422ef063 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -32,7 +32,7 @@ namespace Artemis.Core /// /// The full path to the Artemis data folder /// - public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis.Avalonia"); /// /// The full path to the Artemis logs folder diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 11a34940c..1a8a26905 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -579,10 +579,8 @@ namespace Artemis.Core SKRect bounds = customBounds ?? Bounds; SKPoint positionProperty = Transform.Position.CurrentValue; - // Start at the center of the shape - SKPoint position = zeroBased - ? new SKPoint(bounds.MidX - bounds.Left, bounds.MidY - Bounds.Top) - : new SKPoint(bounds.MidX, bounds.MidY); + // Start at the top left of the shape + SKPoint position = zeroBased ? new SKPoint(0, 0) : new SKPoint(bounds.Left, bounds.Top); // Apply translation if (applyTranslation) @@ -649,9 +647,9 @@ namespace Artemis.Core SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds); SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; - // Translation originates from the unscaled center of the shape and is tied to the anchor - float x = anchorPosition.X - (zeroBased ? bounds.MidX - bounds.Left : bounds.MidX) - anchorProperty.X * bounds.Width; - float y = anchorPosition.Y - (zeroBased ? bounds.MidY - bounds.Top : bounds.MidY) - anchorProperty.Y * bounds.Height; + // Translation originates from the top left of the shape and is tied to the anchor + float x = anchorPosition.X - (zeroBased ? 0 : bounds.Left) - anchorProperty.X * bounds.Width; + float y = anchorPosition.Y - (zeroBased ? 0 : bounds.Top) - anchorProperty.Y * bounds.Height; SKMatrix transform = SKMatrix.Empty; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 6bdf1fd6d..f671d6a07 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using Artemis.Storage.Entities.Profile; @@ -201,33 +202,33 @@ namespace Artemis.Core /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new /// or existing keyframe. /// - /// The new keyframe if one was created. + /// The keyframe if one was created or updated. public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); - LayerPropertyKeyframe? newKeyframe = null; + LayerPropertyKeyframe? keyframe = null; if (time == null || !KeyframesEnabled || !KeyframesSupported) BaseValue = value; else { // If on a keyframe, update the keyframe - LayerPropertyKeyframe? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); + keyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); // Create a new keyframe if none found - if (currentKeyframe == null) + if (keyframe == null) { - newKeyframe = new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this); - AddKeyframe(newKeyframe); + keyframe = new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this); + AddKeyframe(keyframe); } else - currentKeyframe.Value = value; + keyframe.Value = value; } // Force an update so that the base value is applied to the current value and // keyframes/data bindings are applied using the new base value ReapplyUpdate(); - return newKeyframe; + return keyframe; } /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs new file mode 100644 index 000000000..906c5058b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; + +namespace Artemis.Core; + +/// +/// Represents a container for a preview value of a that can be used to update and +/// discard a temporary value. +/// +/// The value type of the layer property. +public sealed class LayerPropertyPreview : IDisposable +{ + /// + /// Creates a new instance of the class. + /// + /// The layer property to apply the preview value to. + /// The time in the timeline at which the preview is applied. + public LayerPropertyPreview(LayerProperty layerProperty, TimeSpan time) + { + Property = layerProperty; + Time = time; + OriginalKeyframe = layerProperty.Keyframes.FirstOrDefault(k => k.Position == time); + OriginalValue = OriginalKeyframe != null ? OriginalKeyframe.Value : layerProperty.CurrentValue; + PreviewValue = OriginalValue; + } + + /// + /// Gets the property this preview applies to. + /// + public LayerProperty Property { get; } + + /// + /// Gets the original keyframe of the property at the time the preview was created. + /// + public LayerPropertyKeyframe? OriginalKeyframe { get; } + + /// + /// Gets the original value of the property at the time the preview was created. + /// + public T OriginalValue { get; } + + /// + /// Gets the time in the timeline at which the preview is applied. + /// + public TimeSpan Time { get; } + + /// + /// Gets the keyframe that was created to preview the value. + /// + public LayerPropertyKeyframe? PreviewKeyframe { get; private set; } + + /// + /// Gets the preview value. + /// + public T? PreviewValue { get; private set; } + + /// + /// Updates the layer property to the given , keeping track of the original state of the + /// property. + /// + /// The value to preview. + public void Preview(T value) + { + PreviewValue = value; + PreviewKeyframe = Property.SetCurrentValue(value, Time); + } + + /// + /// Discard the preview value and restores the original state of the property. The returned boolean can be used to + /// determine whether the preview value was different from the original value. + /// + /// if any changes where discarded; otherwise . + public bool DiscardPreview() + { + if (PreviewKeyframe != null && OriginalKeyframe == null) + { + Property.RemoveKeyframe(PreviewKeyframe); + return true; + } + + Property.SetCurrentValue(OriginalValue, Time); + return !Equals(OriginalValue, PreviewValue); ; + } + + /// + /// Discard the preview value and restores the original state of the property. The returned boolean can be used to + /// determine whether the preview value was different from the original value. + /// + public void Dispose() + { + DiscardPreview(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs index 9a78aba1c..2b2ab624b 100644 --- a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs +++ b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs @@ -12,13 +12,13 @@ namespace Artemis.Core /// /// The point at which the shape is attached to its position /// - [PropertyDescription(Description = "The point at which the shape is attached to its position", InputStepSize = 0.001f)] + [PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%")] public SKPointLayerProperty AnchorPoint { get; set; } /// /// The position of the shape /// - [PropertyDescription(Description = "The position of the shape", InputStepSize = 0.001f)] + [PropertyDescription(Description = "The position of the shape", InputAffix = "%")] public SKPointLayerProperty Position { get; set; } /// @@ -43,6 +43,8 @@ namespace Artemis.Core protected override void PopulateDefaults() { Scale.DefaultValue = new SKSize(100, 100); + AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f); + Position.DefaultValue = new SKPoint(0.5f, 0.5f); Opacity.DefaultValue = 100; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs index 3f0e8fbde..381e3e42d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/Tools/EditToolViewModel.cs @@ -420,7 +420,10 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools private static SKPoint RoundPoint(SKPoint point, int decimals) { - return new((float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero), (float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero)); + return new SKPoint( + (float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero), + (float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero) + ); } private static SKPoint[] UnTransformPoints(SKPoint[] skPoints, Layer layer, SKPoint pivot, bool includeScale) diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs index a1d2cd5d9..9e4b874c0 100644 --- a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs @@ -14,6 +14,9 @@ public static class LayerExtensions /// public static SKRect GetLayerBounds(this Layer layer) { + if (!layer.Leds.Any()) + return SKRect.Empty; + return new SKRect( layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.X), layer.Leds.Min(l => l.RgbLed.AbsoluteBoundary.Location.Y), @@ -32,8 +35,8 @@ public static class LayerExtensions if (positionOverride != null) positionProperty = positionOverride.Value; - // Start at the center of the shape - SKPoint position = new(layerBounds.MidX, layerBounds.MidY); + // Start at the top left of the shape + SKPoint position = new(layerBounds.Left, layerBounds.Top); // Apply translation position.X += positionProperty.X * layerBounds.Width; @@ -59,7 +62,7 @@ public static class LayerExtensions /// /// Returns a new point normalized to 0.0-1.0 /// - public static SKPoint GetScaledPoint(this Layer layer, SKPoint point, bool absolute) + public static SKPoint GetNormalizedPoint(this Layer layer, SKPoint point, bool absolute) { SKRect bounds = GetLayerBounds(layer); if (absolute) diff --git a/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs b/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs new file mode 100644 index 000000000..a8bd9a768 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs @@ -0,0 +1,24 @@ +using Avalonia.Input; + +namespace Artemis.UI.Shared.Providers; + +/// +/// Represents a provider for custom cursors. +/// +public interface ICursorProvider +{ + /// + /// A cursor that indicates a rotating. + /// + public Cursor Rotate { get; } + + /// + /// A cursor that indicates dragging. + /// + public Cursor Drag { get; } + + /// + /// A cursor that indicates dragging horizontally. + /// + public Cursor DragHorizontal { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs new file mode 100644 index 000000000..edc15e9ec --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ResetLayerProperty.cs @@ -0,0 +1,52 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to reset a layer property to it's default value. +/// +public class ResetLayerProperty : IProfileEditorCommand +{ + private readonly LayerProperty _layerProperty; + private readonly T _originalBaseValue; + private readonly bool _keyframesEnabled; + + /// + /// Creates a new instance of the class. + /// + public ResetLayerProperty(LayerProperty layerProperty) + { + if (layerProperty.DefaultValue == null) + throw new ArtemisSharedUIException("Can't reset a layer property without a default value."); + + _layerProperty = layerProperty; + _originalBaseValue = _layerProperty.BaseValue; + _keyframesEnabled = _layerProperty.KeyframesEnabled; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName => "Reset layer property"; + + /// + public void Execute() + { + string json = CoreJson.SerializeObject(_layerProperty.DefaultValue, true); + + if (_keyframesEnabled) + _layerProperty.KeyframesEnabled = false; + + _layerProperty.SetCurrentValue(CoreJson.DeserializeObject(json)!, null); + } + + /// + public void Undo() + { + _layerProperty.SetCurrentValue(_originalBaseValue, null); + if (_keyframesEnabled) + _layerProperty.KeyframesEnabled = true; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs index d6179bb4e..794a64358 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/UpdateLayerProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Artemis.Core; namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; @@ -12,6 +13,7 @@ public class UpdateLayerProperty : IProfileEditorCommand private readonly T _newValue; private readonly T _originalValue; private readonly TimeSpan? _time; + private readonly bool _hasKeyframe; private LayerPropertyKeyframe? _newKeyframe; /// @@ -23,6 +25,7 @@ public class UpdateLayerProperty : IProfileEditorCommand _originalValue = layerProperty.CurrentValue; _newValue = newValue; _time = time; + _hasKeyframe = _layerProperty.Keyframes.Any(k => k.Position == time); DisplayName = $"Update {_layerProperty.PropertyDescription.Name ?? "property"}"; } @@ -50,13 +53,19 @@ public class UpdateLayerProperty : IProfileEditorCommand { // If there was already a keyframe from a previous execute that was undone, put it back if (_newKeyframe != null) + { _layerProperty.AddKeyframe(_newKeyframe); - else + return; + } + + // If the layer had no keyframe yet but keyframes are enabled, a new keyframe will be returned + if (!_hasKeyframe && _layerProperty.KeyframesEnabled) { _newKeyframe = _layerProperty.SetCurrentValue(_newValue, _time); - if (_newKeyframe != null) - DisplayName = $"Add {_layerProperty.PropertyDescription.Name ?? "property"} keyframe"; + DisplayName = $"Add {_layerProperty.PropertyDescription.Name ?? "property"} keyframe"; } + else + _layerProperty.SetCurrentValue(_newValue, _time); } /// diff --git a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs index 6d5b42adc..d527e4d2d 100644 --- a/src/Avalonia/Artemis.UI.Windows/App.axaml.cs +++ b/src/Avalonia/Artemis.UI.Windows/App.axaml.cs @@ -1,4 +1,7 @@ using Artemis.Core.Services; +using Artemis.UI.Shared.Providers; +using Artemis.UI.Windows.Ninject; +using Artemis.UI.Windows.Providers; using Artemis.UI.Windows.Providers.Input; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; @@ -13,7 +16,7 @@ namespace Artemis.UI.Windows { public override void Initialize() { - _kernel = ArtemisBootstrapper.Bootstrap(this); + _kernel = ArtemisBootstrapper.Bootstrap(this, new WindowsModule()); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; AvaloniaXamlLoader.Load(this); } diff --git a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs index a3c1be22d..34c509005 100644 --- a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs @@ -55,7 +55,7 @@ namespace Artemis.UI.Windows public bool FocusExistingInstance() { - _artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535d", out bool createdNew); + _artemisMutex = new Mutex(true, "Artemis-3c24b502-64e6-4587-84bf-9072970e535f", out bool createdNew); if (createdNew) return false; diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 69e2d4bd1..436ab8302 100644 --- a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -8,6 +8,19 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair.png new file mode 100644 index 0000000000000000000000000000000000000000..9330d74cac38eb1cd979b88915afe83ce5c71a76 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!5`og;tHgJ;Me!>zkmGr z|Nno?o!E&$K4VFcUoeBivm0q3PLj8~3rl~-%|IZBy~NYkmHinL50{F_tt!4(Kp`7X z7sn8d^T`Pf433U$Aqq<@JXj54yto8d7#Sx>_9Un^Z!i?#FyN3jGSFgdR9eQ+CgznR zFXQOpq`=IeKbLc|wo+>|&>Yng*NBpo#FA92 ou5DmoWnd82(1)raH$NpatrE8e^}A;FKpPl5UHx3vIVCg!0C-(lk^lez literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_minus.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_minus.png new file mode 100644 index 0000000000000000000000000000000000000000..2845e62887f9314f53fb5f42a0b1ccb262310d9f GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!5`og;tHgJ;Me!>zkmGr z|Nno?o!E&$K4VFcUoeBivm0q3PLj8~3rl~-%|IZBy~NYkmHinL50{F_tt!4(Kp|I8 z7sn8d^T`Pf433U$Aqq<@JXj54yto8d7#Sx>_9Un^Z!i?#FyN3jGSFgdR9eQ+Cgzp1 zJdnXq$(l!pq0Guo)P>E6p<0@2>CL;Z76b-rgDVb@NxHYKXHM0k5VDNPHb6Mw<&;$S_16=h0 literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_plus.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_crosshair_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..b2b0e2e7841c9491b0630a287235a3906e4e6d3e GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!5`og;tHgJ;Me!>zkmGr z|Nno?o!E&$K4VFcUoeBivm0q3PLj8~3rl~-%|IZBy~NYkmHinL50{F_tt!4(K%pQ{ z7sn8d^T`Pf433U$Aqq<@JXj54yto8d7#Sx>_9Un^Z!i?#FyN3jGSFgdR9eQ+Cgzo+ zer~(N>j2gk4h{n@F6A?9+zoU4{8{Uq6|)-OF)(bt%Ux`!FT4n7x@w7QL`h0wNvc(H zQ7VvPFfuSQ(ls#BH8ct_G_W!@vobW-HZZUk4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKpuOE zr>`sfGbSD`6_Hz2e6JW77^^*9978;gzn#2S-#JjCeSUGY`QBU(*A<;5lV{90l3=_c z!oY*0^KL@oV}1q2i5;yGtj)Zg2OT^NBxX%$D^cVK-SYkMR*r?G)zRGt-|IYoGUxu} z?|bAwc>mlt=g$r#+X?d{r!$_t-lPP5f>>k?egxmJ6> z#MIyX#XfAZcbDy-Fsr3TyK%Q&@inRKw~UYRo^JUZxrtk3QM-Yp-pvx}$Mt_-=fC7# z6tn+s)tV|$q^p*=MwFx^mZVxG7o`Fz1|tI_BV7X{T|=V~Ljx;gGb=+2Z36=<1A_%8 g{$-(P$jwj5OsmAL;o?-cYd{SQp00i_>zopr0MmT!i~s-t literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..e222d8e91a85eab3dfbee897b05dcdd2ad039f94 GIT binary patch literal 1124 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081LL6ppAc7|0%X9&#l^|V z2^V2!XP1(aQc_al=jX4hs{=~O%gf8k%CfPs!PUsf$n^L3Z{NP%#Kh#{#f#F?(npRQ zS-yNZP?x2prH+n{sHiAVv6`CN-o1OHqN4Wg+c$agWOH+KpcW@5C!myyii)$dGmrr^ z7N~vJtXV*|y1IH#Pftil2s1M?Q1X$BwOC zyVlm$R#{oOxVX5crbbm&Ra;wo+O%m`u3P~sV`XIps<*bbUa(+6Q&W?KgapJcpc|^I zs}CPOoR*db)B+R)dS6mf66oga?Ciyh7k74c1_uWNT`njn2$Y2=;N#=t<>dv243PWl z*RStizWn(5_4n`JKyFb{(bK0-=gpf33U<=FJ;mH~<|F^m}=E`R&`c=gyrA zbQRF!SFc`u`}Qr+u9q)g9yoBIzP|p>ojV^sd^mXU;QROQzkmO}XV0E*-@bkN^y$x^ zKhe?AU%q@ zbUl4^``7(1&IQRxFn9d@_Xh~SpF6%uS!CkBKYxD*|IT2Un5I7U;6LFbU%tBh`1kLh z-#^>$Jcs}NPVspAE2!u5zsCmh^M3zc!Kk|6+rPittR$Ns{`&Lh`;7_(*7&MIciukb zA5B188I!!-U8Zs*R5b%R>?NMQuI$g4c(_zVZdLKU0!nLox;TbdoK8+yAavr?DL3Jh zC(pW0JbyS~Li-XXjrr4AnQj`KbpUht83R}i^kNuYECrrCFkoDw#6DlzLCZj7!3k#u zWnp7(ZE0(Eb>(jx5}rJI_VDTH=i&k)LSlkP7d5#0#5e`5a)@#Z^NVv7>}FDZdT7z3 zNtZSiiH4e%^19h@X=$xC*}7$;#)XyZR<2#0pOKT5ck#;6kOj;06K>qPcJJcNs}a|G zT^lEE>|B}1&yx_Ckd~L2+3Md|njin4We-8C(bf!f5aCia3QMJT1q9i4;B-JXp zC>2OC7#SED=^7a68XAQd8dw>dSs9vY8yHv_7#x(JzY0Y|ZhlH;S|x4`8=Ta|fEpM) MUHx3vIVCg!06CPe^8f$< literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_pen_min.png new file mode 100644 index 0000000000000000000000000000000000000000..b876e71d66cfafa6d753bc1c3e4697f128cec296 GIT binary patch literal 460 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10!aKkx#1%;60oSfw`}gl( zXlUrehYx>!|88YvB_t$t;=~D7R@RLhH*VRorKqTA+qP{xcI^27|3Al1>uR7`oFzei z!3=&HueEM+^DEak- zar*7K+k6cQ91V$9kuJ|S-TUu5IkNj~=iTe|&wsEnr7f7gJ>msZrcs#@Y2QIe8al4_M)lnSI6j0}v7bPbGj4UIw!4XljKtPD-H4GgRd j3=YcAUxlI}H$NpatrE9}4NmG}Kn)C@u6{1-oD!M!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10!Y{xl#1%;60oSfw`}O_% zzkmNiLqmW6`0?=JLn|vQAt9j?Cr+@kvToeCam$u1MMXv1wr$(7W5@sh|D_yCj{uG1 zED7=pX7JN^t#y-|m+@lpb`PK+W0JSKi{zWUtsOuPdx@v7EBiAh9xfG;TUC6ofI>x{ zE{-7yG-OazVr1Cfqpjk!;^Fl(yg}vr zz8V|Zy_gXHSxkTNb~gS>jf3wBdT;ZF^ynBriPtewdHTgQxz0drarzp!`2OC7#SED=^7a68XAQd w8dw>dSs9vY8yHv_7#x(JzY0Y|ZhlH;S|x4`8=Ta|fEpM)UHx3vIVCg!0H>CkdjJ3c literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..91f5728a5324ed771b6fa24b4e9fa95c20104fea GIT binary patch literal 825 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKpuOE zr>`sfGbSD`6_Hz2e6N7=j-D=#As)x~PVx1a;wW*v{{8OTyWUNDGHZvnYpX!X$_09! zAuOsZ!gZBDDQm3YD}50V`|Dh=ya;Pxh?=Bh)P(7$6c#pz1;}!>9N}3wsmH{){Qlf} z_UTK*uGhvcO6NOl@cFml=QGC4|4nkr9-owpZz?+}c6#&Szm`_Z4c}fgDxAI5SUl&B z!sHBHuFh36x%SA-y|Ca^sjr*tDZzg;wVo9h&CV(OyQAloYjbr8WA7E_fbRCnN~ufC z7vIHQsPHO%79>Tp{zc(sFc?YEA6`-G)jhbpV|7SCQ4xL5Afi@uzPiK(7% zy3ZZsp1WRZf_~ZgFRLpKls8V7@eR%4e;u}n>sV=2-9|YL?Wq@&MPKf26S?3n8EPf^ zc=4*9jRv6`+r(M~0?%!fXxJ1wnME{f>cy)XPdh((a#_7xa;)}Vp_*0ouh7B+{(53& z#UGh|YO=oaZD&)ucKOEv{lux4xEIumo54W62SEb|*}jp5Oc}Z+khxYhQbbWkJ$xJ?odO z>l1Vy-+S{;`egj4_hze_=8L4-$R7MN?|Lv-pX6@kU|xS06^l=4Lid*62`t-jkg@Ce zmv@)9tK3;^^|*WURb4)TFDAd+&5ob5W}jPn@s8sxj=z??)FK#IZ0z{p6~z)087D8$gf%Gk`x&`jIFz{!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0VijX#5@2NF zV`Sn5av7O;fFv0JXabrpMn)c>Y5)KK2l{}OO%Nz6Bqa3q?OS_$`vV6K^!4=>78ZW~ z{8>Umf`^Ca`Sa&XmoANpifU+R`0(L#eSQ7n#fyc7h2Ok+7ZVfn=FOY;@83_JJlWON zm7PO~iJAAsi&p{y0y#N3yLRn*_Uzfcu-?r;3lvL&{DK+G_g;A~^y{ttfxbc;o~7b- zi>;~v+LR4pTQ~rGl7a3lf2zs6p!TaiUB$7C7!;n?9Z5ZxKu=LRq?$7 z3a#*TaSX9I{dVGUp+g2dE>BsU)-NpYyjR^G@bCX(gBgq0-m%!IetwU+^r94i&2J?6taRG>%6_&5td~z~J&_=h+Il0yrnA^7 z$!fNG@T-i!2`^dojBkGL4y!sG`#5d(Y4NTW>$QPJ-`gL*tl2qD(6-k7X<5C^)%6v> zuCCr%wfLryd5yE3%!&5dWj~&aS2Fgd@!R$KU*!b4S+&G9q9i4;B-JXpC>2OC7#SED z=^7a68XAQd8dw>dSs9vX8yHv_7<~BNYL22IH$NpatrE9}##Of6Kn)C@u6{1-oD!M< D{NvYu literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_br.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_br.png new file mode 100644 index 0000000000000000000000000000000000000000..31275d9301b0c4db5bec7a3b82ff98064d76776e GIT binary patch literal 678 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0VijX#5@2NF z1F{*Jco~^^fDBRrL_1IyP&*?diV6S!{|EYjl}%7cNC>FF-roN0+qZpveFqL4kdTo0 z{P}ZXVd2uHOP@b~&cnm=;lt;KhK8u9s5fuk2@4A^Uc9)zzW&9FS4_;j>>NU_uC9|O zPk#UY{hK#$Vq#*RJ$trm*RGtL9037=zLH1cK+_dVg8YIR%=caqdjHG*ZQlW#Lh+?M zR*UP{BwZ&@yDQfG^Y61=IRZ63i@t!|n&j>7qIe{SR}9EuFY)wsWq-!R!=)l}tBUUx zP-v~Ei(`nz>9-S)^Bpn}aCxiJa?krk;`Dpg3%CFOuOXwOsF?fhRO`+kALi+tkLXzM z;moCXiQ!s+jK$-#md?egELjnYMv}m4U&B@2%!28glbfGSez?YiL|$+YQvf;OXk; Jvd$@?2><~d+g1Po literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tl.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tl.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd314a7a853af33ac5c1ea12d7ac694b25502d0 GIT binary patch literal 671 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0WD;Ov6$3Kh zfRT}hkqLQT~t(5Lqo%d51)B>c%DChzI5r*!otGOpFc}TNE|qD zps%m*?c2BZ_Vz%B2?+_YvI#c7O%eri6ib5qf*JOj3*_v2_V?%AX0d6LT_xG-7F+Qw z6)&_o(D&9}=-2xzLN^8afQlKDyxm=twUTnIfE@M`PhVH|XG}a?Dk8V4_+9~pmU+53 zhFF|_JIP(>kO7Zt?q$U<_nbs7R=@xIuf3@-jA!DW*IahK2LN0%irzgVqd_nm+L? zW4o1*{mG2SG-#R9zn`4?8BzR}@1|=A09~wF;u=wsl30>zm0Xkxq!^40jEr;*jC2i+ xLJSS8jLobJ&9n^+tPBi3d~Y>J(U6;;l9^VCTSMb2+isu+22WQ%mvv4FO#qn4)^z{? literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tr.png b/src/Avalonia/Artemis.UI.Windows/Assets/Cursors/aero_rotate_tr.png new file mode 100644 index 0000000000000000000000000000000000000000..6c218cf7cc2b7e1897ceb22856a202a12734a9f5 GIT binary patch literal 669 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10;$(nNh%1n0VijX#5@2NF zV`SoCWaI&gPz8WYW8wuG`{Knbpb!54|NrdSvt7G(<>cfD2naAS^RjaYxw^Veo;>;e z`}c3&yorg4dGqF-u(0sr#f$6f>py(>+|bYv6&1B~>C)%VpY!nWNJvP0{`|SHu&}SM z@4$fr_V)H~-@X+R5(2uGl}*swnmT>9QSvOW)U;>6D28&dW1%^0qGPO@4Rdm-91j>s#Vy+x9uH ztg)D&;Jq7eo*M5GXKS2H7oZ!g*j$8 zb0zS${boBo_2B3Fx*7V$m-wysta+UbbgycOYeY#(Vo9o1a#1RfVlXl=GSW3L(ls;+ wF*L9;HnTD`*ETS)GB5~h=xavNkei>9nO2EggZf=Fd!PmePgg&ebxsLQ065gwzW@LL literal 0 HcmV?d00001 diff --git a/src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs b/src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs new file mode 100644 index 000000000..3a79ea6c3 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Ninject/WindowsModule.cs @@ -0,0 +1,18 @@ +using Artemis.UI.Shared.Providers; +using Artemis.UI.Windows.Providers; +using Ninject.Modules; + +namespace Artemis.UI.Windows.Ninject; + +public class WindowsModule : NinjectModule +{ + #region Overrides of NinjectModule + + /// + public override void Load() + { + Kernel!.Bind().To().InSingletonScope(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs b/src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs new file mode 100644 index 000000000..97a53f032 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Windows/Providers/CursorProvider.cs @@ -0,0 +1,22 @@ +using System; +using Artemis.UI.Shared.Providers; +using Avalonia; +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Artemis.UI.Windows.Providers; + +public class CursorProvider : ICursorProvider +{ + public CursorProvider(IAssetLoader assetLoader) + { + Rotate = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_rotate.png"))), new PixelPoint(21, 10)); + Drag = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_drag.png"))), new PixelPoint(11, 3)); + DragHorizontal = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png"))), new PixelPoint(16, 5)); + } + + public Cursor Rotate { get; } + public Cursor Drag { get; } + public Cursor DragHorizontal { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs index be40ac024..a72152b46 100644 --- a/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Avalonia/Artemis.UI/ArtemisBootstrapper.cs @@ -11,6 +11,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Ninject; +using Ninject.Modules; using ReactiveUI; using Splat.Ninject; @@ -21,7 +22,7 @@ namespace Artemis.UI private static StandardKernel? _kernel; private static Application? _application; - public static StandardKernel Bootstrap(Application application) + public static StandardKernel Bootstrap(Application application, params INinjectModule[] modules) { if (_application != null || _kernel != null) throw new ArtemisUIException("UI already bootstrapped"); @@ -35,6 +36,7 @@ namespace Artemis.UI _kernel.Load(); _kernel.Load(); _kernel.Load(); + _kernel.Load(modules); _kernel.UseNinjectDependencyResolver(); diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs index 5a5eb64c6..1f95bd0dc 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyViewModel.cs @@ -1,4 +1,6 @@ using System; +using System.Reactive; +using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; @@ -27,11 +29,19 @@ internal class TreePropertyViewModel : ActivatableViewModelBase, ITreePropert _profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d); this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d); }); + + ResetToDefault = ReactiveCommand.Create(ExecuteResetToDefault, Observable.Return(LayerProperty.DefaultValue != null)); + } + + private void ExecuteResetToDefault() + { + _profileEditorService.ExecuteCommand(new ResetLayerProperty(LayerProperty)); } public LayerProperty LayerProperty { get; } public PropertyViewModel PropertyViewModel { get; } public PropertyInputViewModel? PropertyInputViewModel { get; } + public ReactiveCommand ResetToDefault { get; } public bool KeyframesEnabled { @@ -62,4 +72,5 @@ internal class TreePropertyViewModel : ActivatableViewModelBase, ITreePropert return depth; } -} \ No newline at end of file +} + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs index e409816dd..802c407a8 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/StatusBar/StatusBarViewModel.cs @@ -30,14 +30,21 @@ public class StatusBarViewModel : ActivatableViewModelBase this.WhenAnyValue(vm => vm.History) .Select(h => h?.Undo ?? Observable.Never()) .Switch() - .Subscribe(c => StatusMessage = c != null ? $"Undid '{c.DisplayName}'." : "Nothing to undo."); + .Subscribe(c => + { + StatusMessage = c != null ? $"Undid '{c.DisplayName}'." : "Nothing to undo."; + ShowStatusMessage = true; + }); this.WhenAnyValue(vm => vm.History) .Select(h => h?.Redo ?? Observable.Never()) .Switch() - .Subscribe(c => StatusMessage = c != null ? $"Redid '{c.DisplayName}'." : "Nothing to redo."); + .Subscribe(c => + { + StatusMessage = c != null ? $"Redid '{c.DisplayName}'." : "Nothing to redo."; + ShowStatusMessage = true; + }); - this.WhenAnyValue(vm => vm.StatusMessage).Subscribe(_ => ShowStatusMessage = true); - this.WhenAnyValue(vm => vm.StatusMessage).Throttle(TimeSpan.FromSeconds(3)).Subscribe(_ => ShowStatusMessage = false); + this.WhenAnyValue(vm => vm.ShowStatusMessage).Where(v => v).Throttle(TimeSpan.FromSeconds(3)).Subscribe(_ => ShowStatusMessage = false); } public RenderProfileElement? ProfileElement => _profileElement?.Value; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml index f0b7c4bdf..8a6ef6906 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml @@ -5,155 +5,193 @@ xmlns:tools="clr-namespace:Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools.TransformToolView" - x:DataType="tools:TransformToolViewModel"> + x:DataType="tools:TransformToolViewModel" + ClipToBounds="False"> - - - - - + + + + + + + + + + + + + + + + + + + - + - - - - + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs index cf78734c3..4799b39cf 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs @@ -1,17 +1,22 @@ using System; +using System.Collections.Generic; using System.Linq; using Artemis.Core; using Artemis.UI.Shared.Extensions; +using Artemis.UI.Shared.Providers; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Media.Transformation; using Avalonia.ReactiveUI; using Avalonia.Skia; -using Avalonia.Styling; +using Avalonia.VisualTree; using ReactiveUI; using SkiaSharp; @@ -19,45 +24,71 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolView : ReactiveUserControl { - private ZoomBorder? _zoomBorder; - private SKPoint _dragStart; + private readonly List _handles = new(); + private readonly Panel _resizeBottomCenter; + private readonly Panel _resizeBottomLeft; + private readonly Panel _resizeBottomRight; + private readonly Panel _resizeLeftCenter; + private readonly Panel _resizeRightCenter; + private readonly Panel _resizeTopCenter; + private readonly Panel _resizeTopLeft; + private readonly Panel _resizeTopRight; + private SKPoint _dragOffset; - - private readonly Ellipse _rotateTopLeft; - private readonly Ellipse _rotateTopRight; - private readonly Ellipse _rotateBottomRight; - private readonly Ellipse _rotateBottomLeft; - - private readonly Rectangle _resizeTopCenter; - private readonly Rectangle _resizeRightCenter; - private readonly Rectangle _resizeBottomCenter; - private readonly Rectangle _resizeLeftCenter; - private readonly Rectangle _resizeTopLeft; - private readonly Rectangle _resizeTopRight; - private readonly Rectangle _resizeBottomRight; - private readonly Rectangle _resizeBottomLeft; - - private readonly Ellipse _anchorPoint; + private ZoomBorder? _zoomBorder; + private readonly Grid _handleGrid; public TransformToolView() { InitializeComponent(); - _rotateTopLeft = this.Get("RotateTopLeft"); - _rotateTopRight = this.Get("RotateTopRight"); - _rotateBottomRight = this.Get("RotateBottomRight"); - _rotateBottomLeft = this.Get("RotateBottomLeft"); + _handleGrid = this.Get("HandleGrid"); - _resizeTopCenter = this.Get("ResizeTopCenter"); - _resizeRightCenter = this.Get("ResizeRightCenter"); - _resizeBottomCenter = this.Get("ResizeBottomCenter"); - _resizeLeftCenter = this.Get("ResizeLeftCenter"); - _resizeTopLeft = this.Get("ResizeTopLeft"); - _resizeTopRight = this.Get("ResizeTopRight"); - _resizeBottomRight = this.Get("ResizeBottomRight"); - _resizeBottomLeft = this.Get("ResizeBottomLeft"); + _handles.Add(this.Get("RotateTopLeft")); + _handles.Add(this.Get("RotateTopRight")); + _handles.Add(this.Get("RotateBottomRight")); + _handles.Add(this.Get("RotateBottomLeft")); - _anchorPoint = this.Get("AnchorPoint"); + _resizeTopCenter = this.Get("ResizeTopCenter"); + _handles.Add(_resizeTopCenter); + _resizeRightCenter = this.Get("ResizeRightCenter"); + _handles.Add(_resizeRightCenter); + _resizeBottomCenter = this.Get("ResizeBottomCenter"); + _handles.Add(_resizeBottomCenter); + _resizeLeftCenter = this.Get("ResizeLeftCenter"); + _handles.Add(_resizeLeftCenter); + _resizeTopLeft = this.Get("ResizeTopLeft"); + _handles.Add(_resizeTopLeft); + _resizeTopRight = this.Get("ResizeTopRight"); + _handles.Add(_resizeTopRight); + _resizeBottomRight = this.Get("ResizeBottomRight"); + _handles.Add(_resizeBottomRight); + _resizeBottomLeft = this.Get("ResizeBottomLeft"); + _handles.Add(_resizeBottomLeft); + + _handles.Add(this.Get("AnchorPoint")); + + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Rotation).Subscribe(_ => UpdateTransforms()).DisposeWith(d)); + } + + private void UpdateTransforms() + { + if (_zoomBorder == null || ViewModel == null) + return; + + double resizeSize = Math.Clamp(1 / _zoomBorder.ZoomX, 0.2, 2); + TransformOperations.Builder builder = TransformOperations.CreateBuilder(2); + builder.AppendScale(resizeSize, resizeSize); + + TransformOperations counterScale = builder.Build(); + RotateTransform counterRotate = new(ViewModel.Rotation * -1); + + // Apply the counter rotation to the containers + foreach (Panel panel in _handleGrid.Children.Where(c => c is Panel and not Canvas).Cast()) + panel.RenderTransform = counterRotate; + + foreach (Control control in _handleGrid.GetVisualDescendants().Where(d => d is Control c && c.Classes.Contains("unscaled")).Cast()) + control.RenderTransform = counterScale; } private void InitializeComponent() @@ -65,6 +96,48 @@ public class TransformToolView : ReactiveUserControl AvaloniaXamlLoader.Load(this); } + private SKPoint GetPositionForViewModel(PointerEventArgs e) + { + if (ViewModel?.Layer == null) + return SKPoint.Empty; + + SKPoint point = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); + return point + _dragOffset; + } + + private static SKPoint CounteractLayerRotation(SKPoint point, Layer layer) + { + SKPoint pivot = layer.GetLayerAnchorPosition(); + + using SKPath counterRotatePath = new(); + counterRotatePath.AddPoly(new[] {SKPoint.Empty, point}, false); + counterRotatePath.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y)); + + return counterRotatePath.Points[1]; + } + + private TransformToolViewModel.ResizeSide GetResizeDirection(Ellipse element) + { + if (ReferenceEquals(element.Parent, _resizeTopLeft)) + return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Left; + if (ReferenceEquals(element.Parent, _resizeTopRight)) + return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(element.Parent, _resizeBottomRight)) + return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(element.Parent, _resizeBottomLeft)) + return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Left; + if (ReferenceEquals(element.Parent, _resizeTopCenter)) + return TransformToolViewModel.ResizeSide.Top; + if (ReferenceEquals(element.Parent, _resizeRightCenter)) + return TransformToolViewModel.ResizeSide.Right; + if (ReferenceEquals(element.Parent, _resizeBottomCenter)) + return TransformToolViewModel.ResizeSide.Bottom; + if (ReferenceEquals(element.Parent, _resizeLeftCenter)) + return TransformToolViewModel.ResizeSide.Left; + + throw new ArgumentException("Given element is not a child of a resize container"); + } + #region Zoom /// @@ -89,43 +162,7 @@ public class TransformToolView : ReactiveUserControl if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null) return; - // TODO - } - - #endregion - - #region Rotation - - private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e) - { - Shape? element = (Shape?) sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) - return; - - _dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint(); - _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); - - e.Pointer.Capture(element); - e.Handled = true; - } - - private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - Shape? element = (Shape?) sender; - if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - e.Pointer.Capture(null); - e.Handled = true; - } - - private void RotationOnPointerMoved(object? sender, PointerEventArgs e) - { - Shape? element = (Shape?) sender; - if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - e.Handled = true; + UpdateTransforms(); } #endregion @@ -134,33 +171,87 @@ public class TransformToolView : ReactiveUserControl private void MoveOnPointerPressed(object? sender, PointerPressedEventArgs e) { - Shape? element = (Shape?) sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) + if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragStart = e.GetCurrentPoint(_zoomBorder).Position.ToSKPoint(); - _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); + SKPoint dragStart = e.GetCurrentPoint(this).Position.ToSKPoint(); + SKRect shapeBounds = ViewModel.Layer.GetLayerPath(true, true, false).Bounds; + _dragOffset = new SKPoint(dragStart.X - shapeBounds.Left, dragStart.Y - shapeBounds.Top); - e.Pointer.Capture(element); - e.Handled = true; - } + ViewModel.StartMovement(); + ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.Position.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.Position.CurrentValue.Y:F3}%"); + ToolTip.SetIsOpen((Control) sender, true); - private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - Shape? element = (Shape?) sender; - if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - e.Pointer.Capture(null); + e.Pointer.Capture((IInputElement?) sender); e.Handled = true; } private void MoveOnPointerMoved(object? sender, PointerEventArgs e) { - Shape? element = (Shape?) sender; - if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + if (!ReferenceEquals(e.Pointer.Captured, sender) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; + SKPoint position = e.GetCurrentPoint(this).Position.ToSKPoint() - _dragOffset; + ViewModel.UpdateMovement(position, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.Position.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.Position.CurrentValue.Y:F3}%"); + + e.Handled = true; + } + + private void MoveOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null) + return; + + ViewModel.FinishMovement(); + ToolTip.SetTip((Control) sender, null); + ToolTip.SetIsOpen((Control) sender, false); + e.Pointer.Capture(null); + e.Handled = true; + } + + #endregion + + #region Anchor movement + + private void AnchorOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) + return; + + SKPoint dragStart = e.GetCurrentPoint(this).Position.ToSKPoint(); + _dragOffset = dragStart - ViewModel.Layer.GetLayerAnchorPosition(); + + ViewModel.StartAnchorMovement(e.GetCurrentPoint(this).Position.ToSKPoint() - _dragOffset); + ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.Y:F3}%"); + ToolTip.SetIsOpen((Control) sender, true); + + e.Pointer.Capture((IInputElement?) sender); + e.Handled = true; + } + + private void AnchorOnPointerMoved(object? sender, PointerEventArgs e) + { + if (!ReferenceEquals(e.Pointer.Captured, sender) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) + return; + + SKPoint position = e.GetCurrentPoint(this).Position.ToSKPoint() - _dragOffset; + ViewModel.UpdateAnchorMovement(position, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + ToolTip.SetTip((Control) sender, $"X: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.X:F3}% Y: {ViewModel.Layer.Transform.AnchorPoint.CurrentValue.Y:F3}%"); + + e.Handled = true; + } + + private void AnchorOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null) + return; + + ViewModel.FinishAnchorMovement(); + ToolTip.SetTip((Control) sender, null); + ToolTip.SetIsOpen((Control) sender, false); + + e.Pointer.Capture(null); e.Handled = true; } @@ -170,86 +261,151 @@ public class TransformToolView : ReactiveUserControl private void ResizeOnPointerPressed(object? sender, PointerPressedEventArgs e) { - Shape? element = (Shape?) sender; - if (element == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) + if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; - _dragStart = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); - _dragOffset = ViewModel.Layer.GetDragOffset(_dragStart); + SKPoint dragStart = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); + _dragOffset = ViewModel.Layer.GetDragOffset(dragStart); + ToolTip.SetTip((Control) sender, $"Width: {ViewModel.Layer.Transform.Scale.CurrentValue.Width:F3}% Height: {ViewModel.Layer.Transform.Scale.CurrentValue.Height:F3}%"); + ToolTip.SetIsOpen((Control) sender, true); - SKPoint position = GetPositionForViewModel(e); - ViewModel.StartResize(position); + ViewModel.StartResize(); - e.Pointer.Capture(element); - e.Handled = true; - } - - private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - Shape? element = (Shape?) sender; - if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null) - return; - - SKPoint position = GetPositionForViewModel(e); - ViewModel.FinishResize(position, GetResizeDirection(element), e.KeyModifiers.HasFlag(KeyModifiers.Shift)); - - e.Pointer.Capture(null); + e.Pointer.Capture((IInputElement?) sender); e.Handled = true; } private void ResizeOnPointerMoved(object? sender, PointerEventArgs e) { - Shape? element = (Shape?) sender; - if (element == null || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) + UpdateCursors(); + if (sender is not Ellipse element || !ReferenceEquals(e.Pointer.Captured, element) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) return; SKPoint position = GetPositionForViewModel(e); - ViewModel.UpdateResize(position, GetResizeDirection(element), e.KeyModifiers.HasFlag(KeyModifiers.Shift)); + ViewModel.UpdateResize( + position, + GetResizeDirection(element), + e.KeyModifiers.HasFlag(KeyModifiers.Alt), + e.KeyModifiers.HasFlag(KeyModifiers.Shift), + e.KeyModifiers.HasFlag(KeyModifiers.Control) + ); + ToolTip.SetTip((Control) sender, $"Width: {ViewModel.Layer.Transform.Scale.CurrentValue.Width:F3}% Height: {ViewModel.Layer.Transform.Scale.CurrentValue.Height:F3}%"); e.Handled = true; } + private void ResizeOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left || ViewModel?.Layer == null) + return; + + ViewModel.FinishResize(); + ToolTip.SetTip((Control) sender, null); + ToolTip.SetIsOpen((Control) sender, false); + + e.Pointer.Capture(null); + e.Handled = true; + } + + private void UpdateCursors() + { + _resizeTopCenter.Cursor = GetCursorAtAngle(0f); + _resizeTopRight.Cursor = GetCursorAtAngle(45f); + _resizeRightCenter.Cursor = GetCursorAtAngle(90f); + _resizeBottomRight.Cursor = GetCursorAtAngle(135f); + _resizeBottomCenter.Cursor = GetCursorAtAngle(180f); + _resizeBottomLeft.Cursor = GetCursorAtAngle(225f); + _resizeLeftCenter.Cursor = GetCursorAtAngle(270f); + _resizeTopLeft.Cursor = GetCursorAtAngle(315f); + } + + private Cursor GetCursorAtAngle(float angle, bool includeLayerRotation = true) + { + if (includeLayerRotation && ViewModel?.Layer != null) + angle = (angle + ViewModel.Layer.Transform.Rotation.CurrentValue) % 360; + + if (angle is > 330 and <= 360 or >= 0 and <= 30) + return new Cursor(StandardCursorType.TopSide); + if (angle is > 30 and <= 60) + return new Cursor(StandardCursorType.TopRightCorner); + if (angle is > 60 and <= 120) + return new Cursor(StandardCursorType.RightSide); + if (angle is > 120 and <= 150) + return new Cursor(StandardCursorType.BottomRightCorner); + if (angle is > 150 and <= 210) + return new Cursor(StandardCursorType.BottomSide); + if (angle is > 210 and <= 240) + return new Cursor(StandardCursorType.BottomLeftCorner); + if (angle is > 240 and <= 300) + return new Cursor(StandardCursorType.LeftSide); + if (angle is > 300 and <= 330) + return new Cursor(StandardCursorType.TopLeftCorner); + + return Cursor.Default; + } + #endregion - private SKPoint GetPositionForViewModel(PointerEventArgs e) + #region Rotation + + private float _rotationDragOffset; + + private void RotationOnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel?.Layer == null) + return; + + float startAngle = CalculateAngleToAnchor(e); + _rotationDragOffset = startAngle - ViewModel.Layer.Transform.Rotation; + ViewModel.StartRotation(); + ToolTip.SetTip((Control)sender, $"{ViewModel.Layer.Transform.Rotation.CurrentValue:F3}"); + ToolTip.SetIsOpen((Control)sender, true); + + e.Pointer.Capture((IInputElement?) sender); + e.Handled = true; + } + + private void RotationOnPointerMoved(object? sender, PointerEventArgs e) + { + if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + float angle = CalculateAngleToAnchor(e); + angle -= _rotationDragOffset; + if (angle < 0) + angle += 360; + + ViewModel?.UpdateRotation(angle, e.KeyModifiers.HasFlag(KeyModifiers.Control)); + ToolTip.SetTip((Control)sender, $"{ViewModel.Layer.Transform.Rotation.CurrentValue:F3}"); + + e.Handled = true; + } + + private void RotationOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (sender == null || !ReferenceEquals(e.Pointer.Captured, sender) || e.InitialPressMouseButton != MouseButton.Left) + return; + + ViewModel?.FinishRotation(); + ToolTip.SetTip((Control)sender, null); + ToolTip.SetIsOpen((Control)sender, false); + + e.Pointer.Capture(null); + e.Handled = true; + } + + private float CalculateAngleToAnchor(PointerEventArgs e) { if (ViewModel?.Layer == null) - return SKPoint.Empty; + return 0; - SKPoint point = CounteractLayerRotation(e.GetCurrentPoint(this).Position.ToSKPoint(), ViewModel.Layer); - return point + _dragOffset; + SKPoint start = ViewModel.Layer.GetLayerAnchorPosition(); + SKPoint arrival = e.GetCurrentPoint(this).Position.ToSKPoint(); + + float radian = (float) Math.Atan2(start.Y - arrival.Y, start.X - arrival.X); + float angle = radian * (180f / (float) Math.PI); + return angle; } - private static SKPoint CounteractLayerRotation(SKPoint point, Layer layer) - { - SKPoint pivot = layer.GetLayerAnchorPosition(); - - using SKPath counterRotatePath = new(); - counterRotatePath.AddPoly(new[] {SKPoint.Empty, point}, false); - counterRotatePath.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y)); - - return counterRotatePath.Points[1]; - } - - private TransformToolViewModel.ResizeSide GetResizeDirection(Shape shape) - { - if (ReferenceEquals(shape, _resizeTopLeft)) - return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Left; - if (ReferenceEquals(shape, _resizeTopRight)) - return TransformToolViewModel.ResizeSide.Top | TransformToolViewModel.ResizeSide.Right; - if (ReferenceEquals(shape, _resizeBottomRight)) - return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Right; - if (ReferenceEquals(shape, _resizeBottomLeft)) - return TransformToolViewModel.ResizeSide.Bottom | TransformToolViewModel.ResizeSide.Left; - if (ReferenceEquals(shape, _resizeTopCenter)) - return TransformToolViewModel.ResizeSide.Top; - if (ReferenceEquals(shape, _resizeRightCenter)) - return TransformToolViewModel.ResizeSide.Right; - if (ReferenceEquals(shape, _resizeBottomCenter)) - return TransformToolViewModel.ResizeSide.Bottom; - if (ReferenceEquals(shape, _resizeLeftCenter)) - return TransformToolViewModel.ResizeSide.Left; - - throw new ArgumentException("Given shape isn't a resize shape"); - } + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs index d78c51f2b..b19d90311 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs @@ -1,9 +1,8 @@ using System; -using System.Diagnostics; -using System.Linq; using System.Reactive; using System.Reactive.Linq; using Artemis.Core; +using Artemis.UI.Exceptions; using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; @@ -17,14 +16,13 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; public class TransformToolViewModel : ToolViewModel { - private readonly IProfileEditorService _profileEditorService; private readonly ObservableAsPropertyHelper _isEnabled; - private RelativePoint _relativeAnchor; - private double _inverseRotation; + private readonly IProfileEditorService _profileEditorService; + private Point _anchor; private ObservableAsPropertyHelper? _layer; + private RelativePoint _relativeAnchor; private double _rotation; private Rect _shapeBounds; - private Point _anchor; private TimeSpan _time; /// @@ -116,12 +114,6 @@ public class TransformToolViewModel : ToolViewModel set => RaiseAndSetIfChanged(ref _rotation, value); } - public double InverseRotation - { - get => _inverseRotation; - set => RaiseAndSetIfChanged(ref _inverseRotation, value); - } - public RelativePoint RelativeAnchor { get => _relativeAnchor; @@ -149,172 +141,324 @@ public class TransformToolViewModel : ToolViewModel return; SKPath path = new(); - path.AddRect(Layer.GetLayerBounds()); - path.Transform(Layer.GetTransformMatrix(false, true, true, false)); + SKRect layerBounds = Layer.GetLayerBounds(); + path.AddRect(layerBounds); + path.Transform(Layer.GetTransformMatrix(false, true, true, false, layerBounds)); ShapeBounds = path.Bounds.ToRect(); Rotation = Layer.Transform.Rotation.CurrentValue; - InverseRotation = Rotation * -1; - // The middle of the element is 0.5/0.5 in Avalonia, in Artemis it's 0.0/0.0 so compensate for that below SKPoint layerAnchor = Layer.Transform.AnchorPoint.CurrentValue; - RelativeAnchor = new RelativePoint(layerAnchor.X + 0.5, layerAnchor.Y + 0.5, RelativeUnit.Relative); + RelativeAnchor = new RelativePoint(layerAnchor.X, layerAnchor.Y, RelativeUnit.Relative); Anchor = new Point(ShapeBounds.Width * RelativeAnchor.Point.X, ShapeBounds.Height * RelativeAnchor.Point.Y); } - #region Resizing - - private SKSize _resizeStartScale; - private bool _hadKeyframe; - - public void StartResize(SKPoint position) - { - if (Layer == null) - return; - - _resizeStartScale = Layer.Transform.Scale.CurrentValue; - _hadKeyframe = Layer.Transform.Scale.Keyframes.Any(k => k.Position == _time); - } - - public void FinishResize(SKPoint position, ResizeSide side, bool evenSides) - { - if (Layer == null) - return; - - // Grab the size one last time - SKSize size = UpdateResize(position, side, evenSides); - - // If the layer has keyframes, new keyframes may have been added while the user was dragging - if (Layer.Transform.Scale.KeyframesEnabled) - { - // If there was already a keyframe at the old spot, edit that keyframe - if (_hadKeyframe) - _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _resizeStartScale, _time)); - // If there was no keyframe yet, remove the keyframe that was created while dragging and create a permanent one - else - { - Layer.Transform.Scale.RemoveKeyframe(Layer.Transform.Scale.Keyframes.First(k => k.Position == _time)); - _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _time)); - } - } - else - { - _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, size, _resizeStartScale, _time)); - } - } - - public SKSize UpdateResize(SKPoint position, ResizeSide side, bool evenSides) - { - if (Layer == null) - return SKSize.Empty; - - SKPoint normalizedAnchor = Layer.Transform.AnchorPoint; - // TODO Remove when anchor is centralized at 0.5,0.5 - normalizedAnchor = new SKPoint(normalizedAnchor.X + 0.5f, normalizedAnchor.Y + 0.5f); - - // The anchor is used to ensure a side can't shrink past the anchor - SKPoint anchor = Layer.GetLayerAnchorPosition(); - // The bounds are used to determine whether to shrink or grow - SKRect shapeBounds = Layer.GetLayerPath(true, true, false).Bounds; - - float width = shapeBounds.Width; - float height = shapeBounds.Height; - - // Resize each side as requested, the sides of each axis are mutually exclusive - if (side.HasFlag(ResizeSide.Left)) - { - if (position.X > anchor.X) - position.X = anchor.X; - - float anchorOffset = 1f - normalizedAnchor.X; - float difference = MathF.Abs(shapeBounds.Left - position.X); - if (position.X < shapeBounds.Left) - width += difference / anchorOffset; - else - width -= difference / anchorOffset; - } - else if (side.HasFlag(ResizeSide.Right)) - { - if (position.X < anchor.X) - position.X = anchor.X; - - float anchorOffset = normalizedAnchor.X; - float difference = MathF.Abs(shapeBounds.Right - position.X); - if (position.X > shapeBounds.Right) - width += difference / anchorOffset; - else - width -= difference / anchorOffset; - } - - if (side.HasFlag(ResizeSide.Top)) - { - if (position.Y > anchor.Y) - position.Y = anchor.Y; - - float anchorOffset = 1f - normalizedAnchor.Y; - float difference = MathF.Abs(shapeBounds.Top - position.Y); - if (position.Y < shapeBounds.Top) - height += difference / anchorOffset; - else - height -= difference / anchorOffset; - } - else if (side.HasFlag(ResizeSide.Bottom)) - { - if (position.Y < anchor.Y) - position.Y = anchor.Y; - - float anchorOffset = normalizedAnchor.Y; - float difference = MathF.Abs(shapeBounds.Bottom - position.Y); - if (position.Y > shapeBounds.Bottom) - height += difference / anchorOffset; - else - height -= difference / anchorOffset; - } - - // Even out the sides to the size of the longest side - if (evenSides) - { - if (width > height) - width = height; - else - height = width; - } - - // Normalize the scale to a percentage - SKRect bounds = Layer.GetLayerBounds(); - width = width / bounds.Width * 100f; - height = height / bounds.Height * 100f; - - Layer.Transform.Scale.SetCurrentValue(new SKSize(width, height), _time); - return new SKSize(width, height); - } - - #endregion - - #region Rotating - - - - #endregion - - #region Movement - - - - #endregion - - #region Anchor movement - - - - #endregion - [Flags] public enum ResizeSide { Top = 1, Right = 2, Bottom = 4, - Left = 8, + Left = 8 } + + #region Movement + + private LayerPropertyPreview? _movementPreview; + + public void StartMovement() + { + if (Layer == null) + return; + + _movementPreview?.DiscardPreview(); + _movementPreview = new LayerPropertyPreview(Layer.Transform.Position, _time); + } + + public void UpdateMovement(SKPoint position, bool stickToAxis, bool round) + { + if (Layer == null) + return; + if (_movementPreview == null) + throw new ArtemisUIException("Can't update movement without a preview having been started"); + + // Get a normalized point + SKPoint scaled = Layer.GetNormalizedPoint(position, true); + // Compensate for the anchor + scaled.X += ((Layer.Transform.AnchorPoint.CurrentValue.X) * (Layer.Transform.Scale.CurrentValue.Width/100f)); + scaled.Y += ((Layer.Transform.AnchorPoint.CurrentValue.Y) * (Layer.Transform.Scale.CurrentValue.Height/100f)); + _movementPreview.Preview(scaled); + } + + public void FinishMovement() + { + if (Layer == null) + return; + if (_movementPreview == null) + throw new ArtemisUIException("Can't update movement without a preview having been started"); + + if (_movementPreview.DiscardPreview()) + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Position, _movementPreview.PreviewValue, _time)); + _movementPreview = null; + } + + #endregion + + #region Anchor movement + + private SKPoint _dragOffset; + private SKPoint _dragStartAnchor; + + private LayerPropertyPreview? _anchorMovementPreview; + + public void StartAnchorMovement(SKPoint position) + { + if (Layer == null) + return; + + // Mouse doesn't care about rotation so get the layer path without rotation + SKPath path = Layer.GetLayerPath(true, true, false); + SKPoint topLeft = path.Points[0]; + // Measure from the top-left of the shape (without rotation) + _dragOffset = topLeft + (position - topLeft); + // Get the absolute layer anchor and make it relative to the unrotated shape + _dragStartAnchor = Layer.GetLayerAnchorPosition() - topLeft; + + _movementPreview?.DiscardPreview(); + _anchorMovementPreview?.DiscardPreview(); + _movementPreview = new LayerPropertyPreview(Layer.Transform.Position, _time); + _anchorMovementPreview = new LayerPropertyPreview(Layer.Transform.AnchorPoint, _time); + } + + public void UpdateAnchorMovement(SKPoint position, bool stickToAxis, bool round) + { + if (Layer == null) + return; + if (_movementPreview == null || _anchorMovementPreview == null) + throw new ArtemisUIException("Can't update movement without a preview having been started"); + + SKPoint topLeft = Layer.GetLayerPath(true, true, true)[0]; + + // The start anchor is relative to an unrotated version of the shape + SKPoint start = _dragStartAnchor; + // Add the current position to the start anchor to determine the new position + SKPoint current = start + (position - _dragOffset); + // In order to keep the mouse movement unrotated, counter-act the active rotation + SKPoint[] countered = UnTransformPoints(new[] {start, current}, Layer, start, true); + + // If shift is held down, round down to 1 decimal to allow moving the anchor in big increments + int decimals = round ? 1 : 3; + SKPoint scaled = RoundPoint(Layer.GetNormalizedPoint(countered[1], false), decimals); + + // Update the anchor point, this causes the shape to move + _anchorMovementPreview.Preview(scaled); + // TopLeft is not updated yet and acts as a snapshot of the top-left before changing the anchor + SKPath path = Layer.GetLayerPath(true, true, true); + // Calculate the (scaled) difference between the old and now position + SKPoint difference = Layer.GetNormalizedPoint(topLeft - path.Points[0], false); + // Apply the difference so that the shape effectively stays in place + _movementPreview.Preview(Layer.Transform.Position.CurrentValue + difference); + } + + public void FinishAnchorMovement() + { + if (Layer == null) + return; + if (_movementPreview == null || _anchorMovementPreview == null) + throw new ArtemisUIException("Can't update movement without a preview having been started"); + + // Not interested in this one's return value but we do need to discard it + _movementPreview.DiscardPreview(); + if (_anchorMovementPreview.DiscardPreview()) + { + using ProfileEditorCommandScope commandScope = _profileEditorService.CreateCommandScope("Update anchor point"); + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Position, _movementPreview.PreviewValue, _time)); + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.AnchorPoint, _anchorMovementPreview.PreviewValue, _time)); + } + + _movementPreview = null; + _anchorMovementPreview = null; + } + + #endregion + + #region Resizing + + private LayerPropertyPreview? _resizePreview; + + public void StartResize() + { + if (Layer == null) + return; + + _resizePreview?.DiscardPreview(); + _resizePreview = new LayerPropertyPreview(Layer.Transform.Scale, _time); + } + + public void UpdateResize(SKPoint position, ResizeSide side, bool evenPercentages, bool evenPixels, bool round) + { + if (Layer == null) + return; + if (_resizePreview == null) + throw new ArtemisUIException("Can't update size without a preview having been started"); + + SKPoint normalizedAnchor = Layer.Transform.AnchorPoint; + normalizedAnchor = new SKPoint(normalizedAnchor.X, normalizedAnchor.Y); + + // The anchor is used to ensure a side can't shrink past the anchor + SKPoint anchor = Layer.GetLayerAnchorPosition(); + SKRect bounds = Layer.GetLayerBounds(); + + float width = Layer.Transform.Scale.CurrentValue.Width; + float height = Layer.Transform.Scale.CurrentValue.Height; + + // Resize each side as requested, the sides of each axis are mutually exclusive + if (side.HasFlag(ResizeSide.Left) && normalizedAnchor.X != 0) + { + if (position.X > anchor.X) + position.X = anchor.X; + + float anchorWeight = normalizedAnchor.X; + + float requiredDistance = anchor.X - position.X; + float requiredSize = requiredDistance / anchorWeight; + width = requiredSize / bounds.Width * 100f; + if (round) + width = MathF.Round(width / 5f) * 5f; + } + else if (side.HasFlag(ResizeSide.Right) && Math.Abs(normalizedAnchor.X - 1) > 0.001) + { + if (position.X < anchor.X) + position.X = anchor.X; + + float anchorWeight = 1f - normalizedAnchor.X; + + float requiredDistance = position.X - anchor.X; + float requiredSize = requiredDistance / anchorWeight; + width = requiredSize / bounds.Width * 100f; + if (round) + width = MathF.Round(width / 5f) * 5f; + } + + if (side.HasFlag(ResizeSide.Top) && normalizedAnchor.Y != 0) + { + if (position.Y > anchor.Y) + position.Y = anchor.Y; + + float anchorWeight = normalizedAnchor.Y; + + float requiredDistance = anchor.Y - position.Y; + float requiredSize = requiredDistance / anchorWeight; + height = requiredSize / bounds.Height * 100f; + if (round) + height = MathF.Round(height / 5f) * 5f; + } + else if (side.HasFlag(ResizeSide.Bottom) && Math.Abs(normalizedAnchor.Y - 1) > 0.001) + { + if (position.Y < anchor.Y) + position.Y = anchor.Y; + + float anchorWeight = 1f - normalizedAnchor.Y; + + float requiredDistance = position.Y - anchor.Y; + float requiredSize = requiredDistance / anchorWeight; + height = requiredSize / bounds.Height * 100f; + if (round) + height = MathF.Round(height / 5f) * 5f; + } + + // Apply side evening but only when resizing on a corner + bool resizingCorner = (side.HasFlag(ResizeSide.Left) || side.HasFlag(ResizeSide.Right)) && (side.HasFlag(ResizeSide.Top) || side.HasFlag(ResizeSide.Bottom)); + if (evenPercentages && resizingCorner) + { + if (width > height) + width = height; + else + height = width; + } + else if (evenPixels && resizingCorner) + { + if (width * bounds.Width > height * bounds.Height) + height = width * bounds.Width / bounds.Height; + else + width = height * bounds.Height / bounds.Width; + } + + _resizePreview.Preview(new SKSize(width, height)); + } + + public void FinishResize() + { + if (Layer == null) + return; + if (_resizePreview == null) + throw new ArtemisUIException("Can't update size without a preview having been started"); + + if (_resizePreview.DiscardPreview()) + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Scale, _resizePreview.PreviewValue, _time)); + _resizePreview = null; + } + + #endregion + + #region Rotating + + private LayerPropertyPreview? _rotatePreview; + + public void StartRotation() + { + if (Layer == null) + return; + + _rotatePreview?.DiscardPreview(); + _rotatePreview = new LayerPropertyPreview(Layer.Transform.Rotation, _time); + } + + public void UpdateRotation(float rotation, bool round) + { + if (_rotatePreview == null) + throw new ArtemisUIException("Can't update rotation without a preview having been started"); + + if (round) + rotation = MathF.Round(rotation / 5f) * 5f; + + _rotatePreview.Preview(rotation); + } + + public void FinishRotation() + { + if (Layer == null) + return; + if (_rotatePreview == null) + throw new ArtemisUIException("Can't update rotation without a preview having been started"); + + if (_rotatePreview.DiscardPreview()) + _profileEditorService.ExecuteCommand(new UpdateLayerProperty(Layer.Transform.Rotation, _rotatePreview.PreviewValue, _time)); + _rotatePreview = null; + } + + #endregion + + #region Utilities + + private static SKPoint RoundPoint(SKPoint point, int decimals) + { + return new SKPoint( + (float) Math.Round(point.X, decimals, MidpointRounding.AwayFromZero), + (float) Math.Round(point.Y, decimals, MidpointRounding.AwayFromZero) + ); + } + + private static SKPoint[] UnTransformPoints(SKPoint[] skPoints, Layer layer, SKPoint pivot, bool includeScale) + { + using SKPath counterRotatePath = new(); + counterRotatePath.AddPoly(skPoints, false); + counterRotatePath.Transform(SKMatrix.CreateRotationDegrees(layer.Transform.Rotation.CurrentValue * -1, pivot.X, pivot.Y)); + if (includeScale) + counterRotatePath.Transform(SKMatrix.CreateScale(1f / (layer.Transform.Scale.CurrentValue.Width / 100f), 1f / (layer.Transform.Scale.CurrentValue.Height / 100f))); + + return counterRotatePath.Points; + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs index c180f00da..31315ae25 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs @@ -91,19 +91,7 @@ public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualiz private void UpdateLayerBounds() { - // Create accurate bounds based on the RgbLeds and not the rounded ArtemisLeds - SKPath path = new(); - foreach (ArtemisLed artemisLed in Layer.Leds) - { - path.AddRect(SKRect.Create( - artemisLed.RgbLed.AbsoluteBoundary.Location.X, - artemisLed.RgbLed.AbsoluteBoundary.Location.Y, - artemisLed.RgbLed.AbsoluteBoundary.Size.Width, - artemisLed.RgbLed.AbsoluteBoundary.Size.Height) - ); - } - - SKRect bounds = path.Bounds; + SKRect bounds = Layer.GetLayerBounds(); LayerBounds = new Rect(0, 0, bounds.Width, bounds.Height); X = bounds.Left; Y = bounds.Top; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs index 32fd84f96..f7e934dc6 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerViewModel.cs @@ -2,11 +2,13 @@ using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; using Artemis.UI.Shared.Services.ProfileEditor; using Avalonia; using Avalonia.Controls.Mixins; using ReactiveUI; using ShimSkiaSharp; +using SKRect = SkiaSharp.SKRect; namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; @@ -59,20 +61,9 @@ public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerVie private void Update() { - // Create accurate bounds based on the RgbLeds and not the rounded ArtemisLeds - SKPath path = new(); - foreach (ArtemisLed artemisLed in Layer.Leds) - { - path.AddRect(SKRect.Create( - artemisLed.RgbLed.AbsoluteBoundary.Location.X, - artemisLed.RgbLed.AbsoluteBoundary.Location.Y, - artemisLed.RgbLed.AbsoluteBoundary.Size.Width, - artemisLed.RgbLed.AbsoluteBoundary.Size.Height) - ); - } - - LayerBounds = new Rect(0, 0, path.Bounds.Width, path.Bounds.Height); - X = path.Bounds.Left; - Y = path.Bounds.Top; + SKRect bounds = Layer.GetLayerBounds(); + LayerBounds = new Rect(0, 0, bounds.Width, bounds.Height); + X = bounds.Left; + Y = bounds.Top; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs index 2d629a2bf..f6f8e6c53 100644 --- a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs @@ -3,24 +3,41 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.DefaultTypes.PropertyInput; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.PropertyInput; +using Avalonia; using DynamicData; +using Ninject; namespace Artemis.UI.Services; public class RegistrationService : IRegistrationService { + private readonly IKernel _kernel; private readonly IInputService _inputService; private readonly IPropertyInputService _propertyInputService; private bool _registeredBuiltInPropertyEditors; - public RegistrationService(IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable toolViewModels) + public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable toolViewModels) { + _kernel = kernel; _inputService = inputService; _propertyInputService = propertyInputService; profileEditorService.Tools.AddRange(toolViewModels); + CreateCursorResources(); + } + + private void CreateCursorResources() + { + ICursorProvider? cursorProvider = _kernel.TryGet(); + if (cursorProvider == null) + return; + + Application.Current?.Resources.Add("RotateCursor", cursorProvider.Rotate); + Application.Current?.Resources.Add("DragCursor", cursorProvider.Drag); + Application.Current?.Resources.Add("DragHorizontalCursor", cursorProvider.DragHorizontal); } public void RegisterBuiltInDataModelDisplays() From b503906b9a63f477591ffc0119559cf8d117d794 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 14 Feb 2022 23:51:55 +0100 Subject: [PATCH 132/270] Profile editor - Added element removal --- src/Artemis.Core/Models/Profile/Folder.cs | 14 +------------ src/Artemis.Core/Models/Profile/Layer.cs | 12 ----------- .../Models/Profile/ProfileElement.cs | 2 +- .../Models/Profile/RenderProfileElement.cs | 12 +---------- .../Commands/RemoveProfileElement.cs | 10 +++++++--- .../Panels/ProfileTree/ProfileTreeView.axaml | 2 +- .../ProfileTree/ProfileTreeView.axaml.cs | 10 ++++++++++ .../Panels/ProfileTree/TreeItemViewModel.cs | 20 ++++++++++++++----- 8 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index e77a5a6ad..96dd20416 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -266,19 +266,7 @@ namespace Artemis.Core /// Occurs when a property affecting the rendering properties of this folder has been updated /// public event EventHandler? RenderPropertiesUpdated; - - /// - public override void Activate() - { - throw new NotImplementedException(); - } - - /// - public override void Deactivate() - { - throw new NotImplementedException(); - } - + /// protected override void Dispose(bool disposing) { diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 1a8a26905..21a8a113f 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -488,18 +488,6 @@ namespace Artemis.Core baseLayerEffect.InternalUpdate(Timeline); } - /// - public override void Activate() - { - throw new NotImplementedException(); - } - - /// - public override void Deactivate() - { - throw new NotImplementedException(); - } - /// public override void Disable() { diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index f94cdce28..9d97fd638 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -182,7 +182,7 @@ namespace Artemis.Core private void StreamlineOrder() { for (int index = 0; index < ChildrenList.Count; index++) - ChildrenList[index].Order = index; + ChildrenList[index].Order = index + 1; } /// diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index 85aec916d..3c7322f97 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -52,17 +52,7 @@ namespace Artemis.Core /// Occurs when a layer effect has been added or removed to this render element /// public event EventHandler? LayerEffectsUpdated; - - /// - /// Activates the render profile element, loading required brushes, effects or anything else needed for rendering - /// - public abstract void Activate(); - - /// - /// Deactivates the render profile element, disposing required brushes, effects or anything else needed for rendering - /// - public abstract void Deactivate(); - + /// protected override void Dispose(bool disposing) { diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs index ddeab19aa..a6289da6b 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/RemoveProfileElement.cs @@ -12,6 +12,7 @@ public class RemoveProfileElement : IProfileEditorCommand, IDisposable private readonly RenderProfileElement _subject; private readonly ProfileElement _target; private bool _isRemoved; + private readonly bool _wasEnabled; /// /// Creates a new instance of the class. @@ -23,7 +24,8 @@ public class RemoveProfileElement : IProfileEditorCommand, IDisposable _subject = subject; _target = _subject.Parent; - _index = _subject.Children.IndexOf(_subject); + _index = _target.Children.IndexOf(_subject); + _wasEnabled = _subject.Enabled; DisplayName = subject switch { @@ -50,15 +52,17 @@ public class RemoveProfileElement : IProfileEditorCommand, IDisposable { _isRemoved = true; _target.RemoveChild(_subject); - _subject.Deactivate(); + _subject.Disable(); } /// public void Undo() { _isRemoved = false; - _subject.Activate(); _target.AddChild(_subject, _index); + + if (_wasEnabled) + _subject.Enable(); } #endregion diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml index 6896dae98..d55cb17d9 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml @@ -8,7 +8,7 @@ x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.ProfileTreeView" x:DataType="profileTree:ProfileTreeViewModel"> - + + - + + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index fa6ffe96d..a088691aa 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -15,11 +15,13 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public class ProfileTreeViewModel : TreeItemViewModel { + private readonly IProfileEditorService _profileEditorService; private TreeItemViewModel? _selectedChild; public ProfileTreeViewModel(IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory, IRgbService rgbService) : base(null, null, windowService, profileEditorService, rgbService, profileEditorVmFactory) { + _profileEditorService = profileEditorService; this.WhenActivated(d => { profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(configuration => @@ -43,8 +45,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree if (model?.ProfileElement is RenderProfileElement renderProfileElement) profileEditorService.ChangeCurrentProfileElement(renderProfileElement); }); + + ClearSelection = ReactiveCommand.Create(() => _profileEditorService.ChangeCurrentProfileElement(null)); } - + + public ReactiveCommand ClearSelection { get; } + public TreeItemViewModel? SelectedChild { get => _selectedChild; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index 489305eb8..d195b186e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; @@ -17,9 +19,9 @@ namespace Artemis.UI.Screens.ProfileEditor.VisualEditor; public class VisualEditorViewModel : ActivatableViewModelBase { + private readonly SourceList _visualizers; private readonly IProfileEditorVmFactory _vmFactory; private ObservableAsPropertyHelper? _profileConfiguration; - private readonly SourceList _visualizers; private ReadOnlyObservableCollection _tools; public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory) @@ -36,10 +38,33 @@ public class VisualEditorViewModel : ActivatableViewModelBase this.WhenActivated(d => { - _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); - profileEditorService.ProfileConfiguration.Subscribe(CreateVisualizers).DisposeWith(d); - profileEditorService.Tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Filter(t => t.IsSelected).Bind(out ReadOnlyObservableCollection tools).Subscribe().DisposeWith(d); + _profileConfiguration = profileEditorService.ProfileConfiguration + .ToProperty(this, vm => vm.ProfileConfiguration) + .DisposeWith(d); + profileEditorService.ProfileConfiguration + .Subscribe(CreateVisualizers) + .DisposeWith(d); + profileEditorService.Tools + .Connect() + .AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Filter(t => t.IsSelected).Bind(out ReadOnlyObservableCollection tools) + .Subscribe() + .DisposeWith(d); Tools = tools; + + this.WhenAnyValue(vm => vm.ProfileConfiguration) + .Select(p => p?.Profile != null + ? Observable.FromEventPattern(x => p.Profile.DescendentAdded += x, x => p.Profile.DescendentAdded -= x) + : Observable.Never>()) + .Switch() + .Subscribe(AddElement) + .DisposeWith(d); + this.WhenAnyValue(vm => vm.ProfileConfiguration) + .Select(p => p?.Profile != null + ? Observable.FromEventPattern(x => p.Profile.DescendentRemoved += x, x => p.Profile.DescendentRemoved -= x) + : Observable.Never>()) + .Switch() + .Subscribe(RemoveElement) + .DisposeWith(d); }); } @@ -54,9 +79,21 @@ public class VisualEditorViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _tools, value); } + private void RemoveElement(EventPattern eventPattern) + { + List visualizers = Visualizers.Where(v => v.ProfileElement == eventPattern.EventArgs.ProfileElement).ToList(); + if (visualizers.Any()) + _visualizers.RemoveMany(visualizers); + } + + private void AddElement(EventPattern eventPattern) + { + if (eventPattern.EventArgs.ProfileElement is Layer layer) + _visualizers.Edit(list => CreateVisualizer(list, layer)); + } + private void CreateVisualizers(ProfileConfiguration? profileConfiguration) { - // TODO: Monitor and respond to new layers/folders and deletions _visualizers.Edit(list => { list.Clear(); diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs index c0134b102..357f99e7e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs @@ -1,7 +1,10 @@ -namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; +using Artemis.Core; + +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; public interface IVisualizerViewModel { + ProfileElement? ProfileElement { get; } double X { get; } double Y { get; } int Order { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml index 703ec1b30..2e53a14e4 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml @@ -14,6 +14,7 @@ + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs index c2e8936c0..426b0a521 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml.cs @@ -2,60 +2,79 @@ using System; using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.Shapes; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers +namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; + +public partial class LayerShapeVisualizerView : ReactiveUserControl { - public partial class LayerShapeVisualizerView : ReactiveUserControl + private ZoomBorder? _zoomBorder; + private readonly Path _layerVisualizerUnbound; + private readonly Path _layerVisualizer; + + public LayerShapeVisualizerView() { - private ZoomBorder? _zoomBorder; - private readonly Path _layerVisualizerUnbound; - private readonly Path _layerVisualizer; + InitializeComponent(); + _layerVisualizer = this.Get("LayerVisualizer"); + _layerVisualizerUnbound = this.Get("LayerVisualizerUnbound"); - public LayerShapeVisualizerView() - { - InitializeComponent(); - _layerVisualizer = this.Get("LayerVisualizer"); - _layerVisualizerUnbound = this.Get("LayerVisualizerUnbound"); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - #region Overrides of TemplatedControl - - /// - protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) - { - _zoomBorder = (ZoomBorder?) this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); - if (_zoomBorder != null) - _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; - base.OnAttachedToLogicalTree(e); - } - - /// - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - if (_zoomBorder != null) - _zoomBorder.PropertyChanged -= ZoomBorderOnPropertyChanged; - base.OnDetachedFromLogicalTree(e); - } - - private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null) - return; - - _layerVisualizer.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX); - _layerVisualizerUnbound.StrokeThickness = _layerVisualizer.StrokeThickness; - } - - #endregion + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Selected).Subscribe(_ => UpdateStrokeThickness()).DisposeWith(d)); } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + #region Overrides of TemplatedControl + + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _zoomBorder = (ZoomBorder?) this.GetLogicalAncestors().FirstOrDefault(l => l is ZoomBorder); + if (_zoomBorder != null) + _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + base.OnAttachedToLogicalTree(e); + } + + /// + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_zoomBorder != null) + _zoomBorder.PropertyChanged -= ZoomBorderOnPropertyChanged; + base.OnDetachedFromLogicalTree(e); + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property != ZoomBorder.ZoomXProperty || _zoomBorder == null) + return; + + UpdateStrokeThickness(); + } + + private void UpdateStrokeThickness() + { + if (_zoomBorder == null) + return; + + if (ViewModel != null && ViewModel.Selected) + { + _layerVisualizer.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX); + _layerVisualizerUnbound.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX); + } + else + { + _layerVisualizer.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX) / 2; + _layerVisualizerUnbound.StrokeThickness = Math.Max(1, 4 / _zoomBorder.ZoomX) / 2; + } + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs index 31315ae25..dc60211d3 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerViewModel.cs @@ -49,6 +49,7 @@ public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualiz } public Layer Layer { get; } + public ProfileElement ProfileElement => Layer; public bool Selected => _selected?.Value ?? false; public Rect LayerBounds @@ -56,7 +57,7 @@ public class LayerShapeVisualizerViewModel : ActivatableViewModelBase, IVisualiz get => _layerBounds; private set => RaiseAndSetIfChanged(ref _layerBounds, value); } - + public double X { get => _x; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml index b5cc6011c..f5040de54 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerVisualizerView.axaml @@ -9,10 +9,19 @@ ClipToBounds="False"> Layer; public bool Selected => _selected?.Value ?? false; public Rect LayerBounds @@ -43,7 +44,7 @@ public class LayerVisualizerViewModel : ActivatableViewModelBase, IVisualizerVie get => _layerBounds; private set => RaiseAndSetIfChanged(ref _layerBounds, value); } - + public double X { get => _x; diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml index 62afce2c8..38c5c633d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml @@ -10,8 +10,8 @@ - - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml index 68803a6db..17cd88467 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml @@ -38,9 +38,6 @@ - @@ -52,7 +49,6 @@ - diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index a28bf40de..bba6fe5c9 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -3,85 +3,75 @@ using System.Collections.ObjectModel; using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Screens.ProfileEditor.MenuBar; using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.Properties; using Artemis.UI.Screens.ProfileEditor.StatusBar; using Artemis.UI.Screens.ProfileEditor.VisualEditor; -using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Shared.Services.ProfileEditor; using DynamicData; using DynamicData.Binding; -using Ninject; using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor +namespace Artemis.UI.Screens.ProfileEditor; + +public class ProfileEditorViewModel : MainScreenViewModel { - public class ProfileEditorViewModel : MainScreenViewModel + private readonly ISettingsService _settingsService; + private ObservableAsPropertyHelper? _history; + private ObservableAsPropertyHelper? _profileConfiguration; + private ReadOnlyObservableCollection? _tools; + + /// + public ProfileEditorViewModel(IScreen hostScreen, + IProfileEditorService profileEditorService, + ISettingsService settingsService, + VisualEditorViewModel visualEditorViewModel, + ProfileTreeViewModel profileTreeViewModel, + ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, + PropertiesViewModel propertiesViewModel, + StatusBarViewModel statusBarViewModel) + : base(hostScreen, "profile-editor") { - private readonly ISettingsService _settingsService; - private ObservableAsPropertyHelper? _profileConfiguration; - private ObservableAsPropertyHelper? _history; - private ReadOnlyObservableCollection _tools; + _settingsService = settingsService; + VisualEditorViewModel = visualEditorViewModel; + ProfileTreeViewModel = profileTreeViewModel; + PropertiesViewModel = propertiesViewModel; + StatusBarViewModel = statusBarViewModel; + TitleBarViewModel = profileEditorTitleBarViewModel; - /// - public ProfileEditorViewModel(IScreen hostScreen, - IProfileEditorService profileEditorService, - ISettingsService settingsService, - VisualEditorViewModel visualEditorViewModel, - ProfileTreeViewModel profileTreeViewModel, - ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, - MenuBarViewModel menuBarViewModel, - PropertiesViewModel propertiesViewModel, - StatusBarViewModel statusBarViewModel) - : base(hostScreen, "profile-editor") + this.WhenActivated(d => { - _settingsService = settingsService; - VisualEditorViewModel = visualEditorViewModel; - ProfileTreeViewModel = profileTreeViewModel; - PropertiesViewModel = propertiesViewModel; - StatusBarViewModel = statusBarViewModel; + _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); + _history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d); + profileEditorService.Tools.Connect() + .Filter(t => t.ShowInToolbar) + .Sort(SortExpressionComparer.Ascending(vm => vm.Order)) + .Bind(out ReadOnlyObservableCollection tools) + .Subscribe() + .DisposeWith(d); + Tools = tools; + }); + } - if (OperatingSystem.IsWindows()) - TitleBarViewModel = profileEditorTitleBarViewModel; - else - MenuBarViewModel = menuBarViewModel; + public VisualEditorViewModel VisualEditorViewModel { get; } + public ProfileTreeViewModel ProfileTreeViewModel { get; } + public PropertiesViewModel PropertiesViewModel { get; } + public StatusBarViewModel StatusBarViewModel { get; } - this.WhenActivated(d => - { - _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d); - _history = profileEditorService.History.ToProperty(this, vm => vm.History).DisposeWith(d); - profileEditorService.Tools.Connect() - .Filter(t => t.ShowInToolbar) - .Sort(SortExpressionComparer.Ascending(vm => vm.Order)) - .Bind(out ReadOnlyObservableCollection tools) - .Subscribe() - .DisposeWith(d); - Tools = tools; - }); - } + public ReadOnlyObservableCollection? Tools + { + get => _tools; + set => RaiseAndSetIfChanged(ref _tools, value); + } - public VisualEditorViewModel VisualEditorViewModel { get; } - public ProfileTreeViewModel ProfileTreeViewModel { get; } - public MenuBarViewModel? MenuBarViewModel { get; } - public PropertiesViewModel PropertiesViewModel { get; } - public StatusBarViewModel StatusBarViewModel { get; } + public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; + public ProfileEditorHistory? History => _history?.Value; + public PluginSetting TreeWidth => _settingsService.GetSetting("ProfileEditor.TreeWidth", 350.0); + public PluginSetting ConditionsHeight => _settingsService.GetSetting("ProfileEditor.ConditionsHeight", 300.0); + public PluginSetting PropertiesHeight => _settingsService.GetSetting("ProfileEditor.PropertiesHeight", 300.0); - public ReadOnlyObservableCollection Tools - { - get => _tools; - set => RaiseAndSetIfChanged(ref _tools, value); - } - - public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; - public ProfileEditorHistory? History => _history?.Value; - public PluginSetting TreeWidth => _settingsService.GetSetting("ProfileEditor.TreeWidth", 350.0); - public PluginSetting ConditionsHeight => _settingsService.GetSetting("ProfileEditor.ConditionsHeight", 300.0); - public PluginSetting PropertiesHeight => _settingsService.GetSetting("ProfileEditor.PropertiesHeight", 300.0); - - public void OpenUrl(string url) - { - Utilities.OpenUrl(url); - } + public void OpenUrl(string url) + { + Utilities.OpenUrl(url); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml index 3c98dac8b..1bfa0720c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/DefaultTitleBarView.axaml @@ -5,7 +5,7 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Root.DefaultTitleBarView"> - \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml index 991a035dd..c3fcadfac 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/RootView.axaml @@ -5,17 +5,9 @@ xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Root.RootView"> - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml index de19a9df5..1adb77b24 100644 --- a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml @@ -9,7 +9,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index 7e0c573de..2f3bb876c 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -85,13 +85,15 @@ }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.1.8, )", - "resolved": "1.1.8", - "contentHash": "pWxi0zvl4+602rffgZgRIS2srUr/bKFCH/duiV72UodmMp291vaWLC3Lzbp3j5TzSuPHYAlcUBIFvEMlnu8WLQ==", + "requested": "[1.2.1, )", + "resolved": "1.2.1", + "contentHash": "IH9eei7CrOUkUdxL2E/HZYKFgNupSVO+ju74CnVqmV7u7iolyz3g1cTHELqVgatEb+IqXw7KyeLr2459nUxYSw==", "dependencies": { - "Avalonia": "0.10.11", - "Avalonia.Desktop": "0.10.11", - "Avalonia.Diagnostics": "0.10.11" + "Avalonia": "0.10.12", + "Avalonia.Desktop": "0.10.12", + "Avalonia.Diagnostics": "0.10.12", + "MicroCom.CodeGenerator.MSBuild": "0.10.4", + "MicroCom.Runtime": "0.10.4" } }, "Flurl.Http": { @@ -387,6 +389,16 @@ "Microsoft.Extensions.DependencyModel": "5.0.0" } }, + "MicroCom.CodeGenerator.MSBuild": { + "type": "Transitive", + "resolved": "0.10.4", + "contentHash": "aG1kLtkgX6lC8qpxVon4OFSCdWYEbQubIg+2/ychWTIFTrDHWFkhcC4YTn0IfGiVCLwh0Yj7eSc8nk5f3UoMKg==" + }, + "MicroCom.Runtime": { + "type": "Transitive", + "resolved": "0.10.4", + "contentHash": "enc2U+/1UnF3rtocxb5ofcg7cJSmJI4adbYPr8DZa5bQzvhqA/VbjlcalxoqjI3CR2RvM5WWpjKT0p3BriFJjw==" + }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Transitive", "resolved": "2.9.6", @@ -1764,7 +1776,7 @@ "Avalonia.Xaml.Interactions": "0.10.12", "Avalonia.Xaml.Interactivity": "0.10.12", "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.1.8", + "FluentAvaloniaUI": "1.2.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", "ReactiveUI": "17.1.17", From 2a6ed8cc7f586f152e5bf63666b6a680350bff8c Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 18 Feb 2022 16:42:42 +0100 Subject: [PATCH 135/270] UI - Theme fixes --- .../Artemis.UI.Shared/Styles/Border.axaml | 8 ++- .../Artemis.UI/Screens/Home/HomeView.axaml | 3 +- .../Screens/Plugins/PluginSettingsView.axaml | 2 +- .../Settings/Tabs/GeneralTabView.axaml | 54 ++++++++++--------- .../Settings/Tabs/GeneralTabViewModel.cs | 18 +++++++ 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml index 54e2cecf1..59afbcfef 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml @@ -22,6 +22,8 @@ @@ -36,17 +38,21 @@ + + + - + - + + + ShowAcceptDismissButtons="False" /> \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs index 95c3e2618..13922a4fc 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -17,4 +18,4 @@ namespace Artemis.UI.DefaultTypes.PropertyInput AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml index be30ad2bd..f1ef33ff0 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml @@ -3,8 +3,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:menuBar="clr-namespace:Artemis.UI.Screens.ProfileEditor.MenuBar" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView"> + x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView" + x:DataType="menuBar:MenuBarViewModel"> @@ -38,11 +40,11 @@ - + + IsChecked="{CompiledBinding IsSuspended}"/> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index df9021b49..558881f8d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -1,5 +1,8 @@ using System; using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; @@ -8,11 +11,24 @@ namespace Artemis.UI.Screens.ProfileEditor.MenuBar; public class MenuBarViewModel : ActivatableViewModelBase { + private readonly IProfileService _profileService; private ProfileEditorHistory? _history; + private ObservableAsPropertyHelper? _profileConfiguration; + private ObservableAsPropertyHelper? _isSuspended; - public MenuBarViewModel(IProfileEditorService profileEditorService) + public MenuBarViewModel(IProfileEditorService profileEditorService, IProfileService profileService) { - this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); + _profileService = profileService; + this.WhenActivated(d => + { + profileEditorService.History.Subscribe(history => History = history).DisposeWith(d); + _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration); + _isSuspended = profileEditorService.ProfileConfiguration + .Select(p => p?.WhenAnyValue(c => c.IsSuspended) ?? Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.IsSuspended) + .DisposeWith(d); + }); } public ProfileEditorHistory? History @@ -20,4 +36,19 @@ public class MenuBarViewModel : ActivatableViewModelBase get => _history; set => RaiseAndSetIfChanged(ref _history, value); } + + public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; + + public bool IsSuspended + { + get => _isSuspended?.Value ?? false; + set + { + if (ProfileConfiguration == null) + return; + + ProfileConfiguration.IsSuspended = value; + _profileService.SaveProfileCategory(ProfileConfiguration.Category); + } + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index d195b186e..819246bc8 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -52,15 +52,17 @@ public class VisualEditorViewModel : ActivatableViewModelBase Tools = tools; this.WhenAnyValue(vm => vm.ProfileConfiguration) - .Select(p => p?.Profile != null - ? Observable.FromEventPattern(x => p.Profile.DescendentAdded += x, x => p.Profile.DescendentAdded -= x) + .Select(p => p?.Profile) + .Select(p => p != null + ? Observable.FromEventPattern(x => p.DescendentAdded += x, x => p.DescendentAdded -= x) : Observable.Never>()) .Switch() .Subscribe(AddElement) .DisposeWith(d); this.WhenAnyValue(vm => vm.ProfileConfiguration) - .Select(p => p?.Profile != null - ? Observable.FromEventPattern(x => p.Profile.DescendentRemoved += x, x => p.Profile.DescendentRemoved -= x) + .Select(p => p?.Profile) + .Select(p => p != null + ? Observable.FromEventPattern(x => p.DescendentRemoved += x, x => p.DescendentRemoved -= x) : Observable.Never>()) .Switch() .Subscribe(RemoveElement) diff --git a/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml index accd393f5..a802cd821 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml @@ -14,7 +14,7 @@ ExtendClientAreaToDecorationsHint="True" ExtendClientAreaTitleBarHeightHint="450"> - + @@ -22,7 +22,6 @@ Artemis is initializing... diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml index 274660d54..1c6ad837b 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml @@ -83,7 +83,7 @@ diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml index 1462abef4..42c77832e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml @@ -5,8 +5,10 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" + xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView"> + x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView" + x:DataType="sidebar:SidebarProfileConfigurationViewModel"> @@ -65,17 +67,11 @@ - - - - - - - + @@ -85,7 +81,7 @@ Margin="10 0 0 0" VerticalAlignment="Center" HorizontalAlignment="Left" - Text="{Binding ProfileConfiguration.Name}" + Text="{CompiledBinding ProfileConfiguration.Name}" TextTrimming="CharacterEllipsis" /> + IsChecked="{CompiledBinding IsSuspended}"> diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index 827c608d8..b5a1bfc40 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -1,16 +1,20 @@ -using System.Threading.Tasks; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; namespace Artemis.UI.Screens.Sidebar { - public class SidebarProfileConfigurationViewModel : ViewModelBase + public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase { private readonly SidebarViewModel _sidebarViewModel; private readonly IProfileService _profileService; private readonly IWindowService _windowService; + private ObservableAsPropertyHelper? _isSuspended; public ProfileConfiguration ProfileConfiguration { get; } public SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration, IProfileService profileService, IWindowService windowService) @@ -18,8 +22,15 @@ namespace Artemis.UI.Screens.Sidebar _sidebarViewModel = sidebarViewModel; _profileService = profileService; _windowService = windowService; + ProfileConfiguration = profileConfiguration; + this.WhenActivated(d => + { + _isSuspended = ProfileConfiguration.WhenAnyValue(c => c.IsSuspended) + .ToProperty(this, vm => vm.IsSuspended) + .DisposeWith(d); + }); _profileService.LoadProfileConfigurationIcon(ProfileConfiguration); } @@ -27,7 +38,7 @@ namespace Artemis.UI.Screens.Sidebar public bool IsSuspended { - get => ProfileConfiguration.IsSuspended; + get => _isSuspended?.Value ?? false; set { ProfileConfiguration.IsSuspended = value; From 1262f84a568d38eabd5b28f58673184f6ba5c4fc Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 23 Feb 2022 21:34:55 +0100 Subject: [PATCH 137/270] Gradient editor - Initial implementation --- .../Models/Profile/Colors/ColorGradient.cs | 86 +++++++- .../Controls/GradientPicker/GradientPicker.cs | 135 ++++++++++++ .../GradientPicker/GradientPickerColorStop.cs | 199 ++++++++++++++++++ .../ColorGradientToGradientStopsConverter.cs | 41 ++++ .../ParentWidthPercentageConverter.cs | 34 +++ .../Converters/SKColorToStringConverter.cs | 2 +- .../Converters/WidthNormalizedConverter.cs | 29 +++ .../Services/GradientPickerService.cs | 11 + .../Artemis.UI.Shared/Styles/Artemis.axaml | 2 + .../Styles/Controls/GradientPicker.axaml | 192 +++++++++++++++++ .../SKColorPropertyInputView.axaml | 3 +- .../Screens/Workshop/WorkshopView.axaml | 3 + .../Screens/Workshop/WorkshopViewModel.cs | 3 + 13 files changed, 733 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs rename src/Avalonia/{Artemis.UI => Artemis.UI.Shared}/Converters/SKColorToStringConverter.cs (95%) create mode 100644 src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/GradientPickerService.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPicker.axaml diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index c621eb5c7..589b7f5ba 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -11,7 +11,7 @@ namespace Artemis.Core /// /// A gradient containing a list of s /// - public class ColorGradient : IList, INotifyCollectionChanged + public class ColorGradient : IList, IList, INotifyCollectionChanged { private static readonly SKColor[] FastLedRainbow = { @@ -173,13 +173,25 @@ namespace Artemis.Core internal void Sort() { - _stops.Sort((a, b) => a.Position.CompareTo(b.Position)); + int requiredIndex = 0; + foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList()) + { + int actualIndex = _stops.IndexOf(colorGradientStop); + if (requiredIndex != actualIndex) + { + _stops.RemoveAt(actualIndex); + _stops.Insert(requiredIndex, colorGradientStop); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex)); + } + + requiredIndex++; + } } private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) { Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + OnStopChanged(); } #region Implementation of IEnumerable @@ -209,8 +221,16 @@ namespace Artemis.Core OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _stops.IndexOf(item))); } - /// + public int Add(object? value) + { + if (value is ColorGradientStop stop) + _stops.Add(stop); + + return IndexOf(value); + } + + /// public void Clear() { foreach (ColorGradientStop item in _stops) @@ -219,6 +239,32 @@ namespace Artemis.Core OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + /// + public bool Contains(object? value) + { + return _stops.Contains(value); + } + + /// + public int IndexOf(object? value) + { + return _stops.IndexOf(value); + } + + /// + public void Insert(int index, object? value) + { + if (value is ColorGradientStop stop) + _stops.Insert(index, stop); + } + + /// + public void Remove(object? value) + { + if (value is ColorGradientStop stop) + _stops.Remove(stop); + } + /// public bool Contains(ColorGradientStop item) { @@ -244,11 +290,29 @@ namespace Artemis.Core } /// + public void CopyTo(Array array, int index) + { + _stops.CopyTo((ColorGradientStop[]) array, index); + } + + /// public int Count => _stops.Count; /// + public bool IsSynchronized => false; + + /// + public object SyncRoot => this; + + /// public bool IsReadOnly => false; + object? IList.this[int index] + { + get => this[index]; + set => this[index] = (ColorGradientStop) value!; + } + #endregion #region Implementation of IList @@ -268,7 +332,7 @@ namespace Artemis.Core OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); } - /// + /// public void RemoveAt(int index) { _stops[index].PropertyChanged -= ItemOnPropertyChanged; @@ -276,6 +340,8 @@ namespace Artemis.Core OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); } + public bool IsFixedSize { get; } + /// public ColorGradientStop this[int index] { @@ -304,5 +370,15 @@ namespace Artemis.Core } #endregion + + /// + /// Occurs when any of the stops has changed in some way + /// + public event EventHandler? StopChanged; + + private void OnStopChanged() + { + StopChanged?.Invoke(this, EventArgs.Empty); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs new file mode 100644 index 000000000..8359568f5 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Specialized; +using System.Linq; +using Artemis.Core; +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Media; + +namespace Artemis.UI.Shared.Controls.GradientPicker; + +public class GradientPicker : TemplatedControl +{ + private LinearGradientBrush _linearGradientBrush = new(); + + /// + /// Gets or sets the color gradient. + /// + public static readonly StyledProperty ColorGradientProperty = + AvaloniaProperty.Register(nameof(Core.ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf()); + + /// + /// Gets or sets the currently selected color stop. + /// + public static readonly StyledProperty SelectedColorStopProperty = + AvaloniaProperty.Register(nameof(SelectedColorStop), defaultBindingMode: BindingMode.TwoWay); + + /// + /// Gets the linear gradient brush representing the color gradient. + /// + public static readonly DirectProperty LinearGradientBrushProperty = + AvaloniaProperty.RegisterDirect(nameof(LinearGradientBrush), g => g.LinearGradientBrush); + + /// + /// Gets or sets the color gradient. + /// + public ColorGradient ColorGradient + { + get => GetValue(ColorGradientProperty); + set => SetValue(ColorGradientProperty, value); + } + + /// + /// Gets or sets the currently selected color stop. + /// + public ColorGradientStop? SelectedColorStop + { + get => GetValue(SelectedColorStopProperty); + set => SetValue(SelectedColorStopProperty, value); + } + + /// + /// Gets the linear gradient brush representing the color gradient. + /// + public LinearGradientBrush LinearGradientBrush + { + get => _linearGradientBrush; + private set => SetAndRaise(LinearGradientBrushProperty, ref _linearGradientBrush, value); + } + + private static void ColorGradientChanged(IAvaloniaObject sender, bool before) + { + if (before) + (sender as GradientPicker)?.Unsubscribe(); + else + (sender as GradientPicker)?.Subscribe(); + } + + private void Subscribe() + { + ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged; + ColorGradient.StopChanged += ColorGradientOnStopChanged; + + UpdateGradient(); + SelectedColorStop = ColorGradient.FirstOrDefault(); + } + + private void Unsubscribe() + { + ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged; + ColorGradient.StopChanged -= ColorGradientOnStopChanged; + } + + private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateGradient(); + } + + private void ColorGradientOnStopChanged(object? sender, EventArgs e) + { + UpdateGradient(); + } + + private void UpdateGradient() + { + // Remove old stops + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (ColorGradient == null) + return; + + // Add new stops + + // Update the display gradient + GradientStops collection = new(); + foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position)) + collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position)); + LinearGradientBrush = new LinearGradientBrush {GradientStops = collection}; + } + + private void SelectColorStop(object? sender, PointerReleasedEventArgs e) + { + if (sender is IDataContextProvider dataContextProvider && dataContextProvider.DataContext is ColorGradientStop colorStop) + SelectedColorStop = colorStop; + } + + #region Overrides of Visual + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + Subscribe(); + base.OnAttachedToVisualTree(e); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + Unsubscribe(); + base.OnDetachedFromVisualTree(e); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs new file mode 100644 index 000000000..d4872c39a --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs @@ -0,0 +1,199 @@ +using System; +using Artemis.Core; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace Artemis.UI.Shared.Controls.GradientPicker; + +public class GradientPickerColorStop : TemplatedControl +{ + private static ColorGradientStop? _draggingStop; + private static IPointer? _dragPointer; + + /// + /// Gets or sets the gradient picker. + /// + public static readonly StyledProperty GradientPickerProperty = + AvaloniaProperty.Register(nameof(GradientPicker), notifying: Notifying); + + private static void Notifying(IAvaloniaObject sender, bool before) + { + if (sender is not GradientPickerColorStop self) + return; + + if (before && self.GradientPicker != null) + self.GradientPicker.PropertyChanged -= self.GradientPickerOnPropertyChanged; + else if (self.GradientPicker != null) + self.GradientPicker.PropertyChanged += self.GradientPickerOnPropertyChanged; + + self.IsSelected = self.GradientPicker?.SelectedColorStop == self.ColorStop; + } + + /// + /// Gets or sets the color stop. + /// + public static readonly StyledProperty ColorStopProperty = + AvaloniaProperty.Register(nameof(ColorStop)); + + /// + /// Gets or sets the position reference to use when positioning and dragging this color stop. + /// If then dragging is not enabled. + /// + public static readonly StyledProperty PositionReferenceProperty = + AvaloniaProperty.Register(nameof(PositionReference)); + + /// + /// Gets the linear gradient brush representing the color gradient. + /// + public static readonly DirectProperty IsSelectedProperty = + AvaloniaProperty.RegisterDirect(nameof(IsSelected), g => g.IsSelected); + + private bool _isSelected; + private double _dragOffset; + + /// + /// Gets or sets the gradient picker. + /// + public GradientPicker? GradientPicker + { + get => GetValue(GradientPickerProperty); + set => SetValue(GradientPickerProperty, value); + } + + /// + /// Gets or sets the color stop. + /// + public ColorGradientStop ColorStop + { + get => GetValue(ColorStopProperty); + set => SetValue(ColorStopProperty, value); + } + + /// + /// Gets or sets the position reference to use when positioning and dragging this color stop. + /// If then dragging is not enabled. + /// + public IControl? PositionReference + { + get => GetValue(PositionReferenceProperty); + set => SetValue(PositionReferenceProperty, value); + } + + /// + /// Gets the linear gradient brush representing the color gradient. + /// + public bool IsSelected + { + get => _isSelected; + private set + { + SetAndRaise(IsSelectedProperty, ref _isSelected, value); + if (IsSelected) + PseudoClasses.Add(":selected"); + else + PseudoClasses.Remove(":selected"); + } + } + + private void GradientPickerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (GradientPicker != null && e.Property == GradientPicker.SelectedColorStopProperty) + { + IsSelected = GradientPicker.SelectedColorStop == ColorStop; + } + } + + private void OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || PositionReference == null) + return; + + _dragOffset = e.GetCurrentPoint(PositionReference).Position.X - GetPixelPosition(); + e.Pointer.Capture(this); + + // Store these in case the control is being recreated due to an array resort + // it's a bit ugly but it gives us a way to pick up dragging again with the new control + _dragPointer = e.Pointer; + _draggingStop = ColorStop; + e.Handled = true; + + if (GradientPicker != null) + GradientPicker.SelectedColorStop = ColorStop; + } + + private void OnPointerMoved(object? sender, PointerEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || !ReferenceEquals(e.Pointer.Captured, this) || PositionReference == null) + { + if (_draggingStop != ColorStop) + return; + + _dragOffset = e.GetCurrentPoint(PositionReference).Position.X - GetPixelPosition(); + } + + double position = e.GetCurrentPoint(PositionReference).Position.X - _dragOffset; + ColorStop.Position = MathF.Round((float) Math.Clamp(position / PositionReference.Bounds.Width, 0, 1), 3, MidpointRounding.AwayFromZero); + e.Handled = true; + } + + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.InitialPressMouseButton != MouseButton.Left) + return; + + e.Pointer.Capture(null); + e.Handled = true; + _draggingStop = null; + } + + private double GetPixelPosition() + { + if (PositionReference == null) + return 0; + + return PositionReference.Bounds.Width * ColorStop.Position; + } + + #region Overrides of Visual + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + if (GradientPicker != null) + GradientPicker.PropertyChanged += GradientPickerOnPropertyChanged; + + if (PositionReference != null) + { + PointerPressed += OnPointerPressed; + PointerMoved += OnPointerMoved; + PointerReleased += OnPointerReleased; + + // If this stop was previously being dragged, carry on dragging again + // This can happen if the control was recreated due to an array sort + if (_draggingStop == ColorStop && _dragPointer != null) + { + _dragPointer.Capture(this); + IsSelected = true; + } + } + + base.OnAttachedToVisualTree(e); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + if (GradientPicker != null) + GradientPicker.PropertyChanged -= GradientPickerOnPropertyChanged; + PointerPressed -= OnPointerPressed; + PointerMoved -= OnPointerMoved; + PointerReleased -= OnPointerReleased; + + base.OnDetachedFromVisualTree(e); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs new file mode 100644 index 000000000..29f562d47 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs @@ -0,0 +1,41 @@ +using System; +using System.Globalization; +using System.Linq; +using Artemis.Core; +using Avalonia.Data.Converters; +using Avalonia.Media; +using SkiaSharp; + +namespace Artemis.UI.Shared.Converters; + +/// +/// Converts into a . +/// +public class ColorGradientToGradientStopsConverter : IValueConverter +{ + /// + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + ColorGradient? colorGradient = value as ColorGradient; + GradientStops collection = new(); + if (colorGradient == null) + return collection; + + foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position)) + collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position)); + return collection; + } + + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + GradientStops? collection = value as GradientStops; + ColorGradient colorGradients = new(); + if (collection == null) + return colorGradients; + + foreach (GradientStop c in collection.OrderBy(s => s.Offset)) + colorGradients.Add(new ColorGradientStop(new SKColor(c.Color.R, c.Color.G, c.Color.B, c.Color.A), (float) c.Offset)); + return colorGradients; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs new file mode 100644 index 000000000..2d8219d0e --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Shared.Converters; + +/// +/// Converts the width in percentage to a real number based on the width of the given parent +/// +public class ParentWidthPercentageConverter : IValueConverter +{ + #region Implementation of IValueConverter + + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is not IControl parent || value is not double percentage) + return value; + + return parent.Width / 100.0 * percentage; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (parameter is not IControl parent || value is not double real) + return value; + + return real / parent.Width * 100.0; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs similarity index 95% rename from src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs rename to src/Avalonia/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs index e7707d8f1..7837c188e 100644 --- a/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs +++ b/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs @@ -3,7 +3,7 @@ using System.Globalization; using Avalonia.Data.Converters; using SkiaSharp; -namespace Artemis.UI.Converters; +namespace Artemis.UI.Shared.Converters; /// /// diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs new file mode 100644 index 000000000..3e3021ecf --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Shared.Converters; + +/// +/// Converts the width in percentage to a real number based on the width of the given parent +/// +public class WidthNormalizedConverter : IMultiValueConverter +{ + #region Implementation of IMultiValueConverter + + /// + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + object? first = values.FirstOrDefault(); + object? second = values.Skip(1).FirstOrDefault(); + if (first is float value && second is double totalWidth) + return (totalWidth / 1.0) * value; + + return 0.0; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/GradientPickerService.cs b/src/Avalonia/Artemis.UI.Shared/Services/GradientPickerService.cs new file mode 100644 index 000000000..fd5836535 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/GradientPickerService.cs @@ -0,0 +1,11 @@ +using Artemis.UI.Shared.Services.Interfaces; + +namespace Artemis.UI.Shared.Services; + +public class GradientPickerService : IGradientPickerService +{ +} + +public interface IGradientPickerService : IArtemisSharedUIService +{ +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index f77636350..1ee1b2a94 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -32,6 +32,8 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml index 0f271bc78..a46f7fa29 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml @@ -5,6 +5,7 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" + xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.SKColorPropertyInputView" x:DataType="propertyInput:SKColorPropertyInputViewModel"> @@ -44,7 +45,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml index e3a16ed34..cff2680bd 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -7,6 +7,7 @@ xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared" xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop" + xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.WorkshopView" x:DataType="workshop:WorkshopViewModel"> @@ -45,6 +46,8 @@ + + diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 95778a15f..97e951bf0 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -1,5 +1,6 @@ using System.Reactive; using System.Reactive.Linq; +using Artemis.Core; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Input; @@ -32,6 +33,8 @@ namespace Artemis.UI.Screens.Workshop public Cursor Cursor => _cursor.Value; + public ColorGradient ColorGradient { get; set; } = ColorGradient.GetUnicornBarf(); + private void ExecuteShowNotification(NotificationSeverity severity) { _notificationService.CreateNotification().WithTitle("Test title").WithMessage("Test message").WithSeverity(severity).Show(); From 8806348386e2c7dbfaf931f1b681ec75b60e8573 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 27 Feb 2022 18:37:33 +0100 Subject: [PATCH 138/270] Gradient editor - Implemented picker button and layer property editor --- .../Properties/ColorGradientLayerProperty.cs | 12 +- .../Models/Profile/Colors/ColorGradient.cs | 920 +++++++++++------- .../Profile/Colors/ColorGradientStop.cs | 31 +- .../Controls/Flyouts/GradientPickerFlyout.cs | 25 + .../Controls/GradientPicker/GradientPicker.cs | 255 ++++- .../GradientPicker/GradientPickerButton.cs | 212 ++++ .../GradientPicker/GradientPickerColorStop.cs | 2 +- .../Converters/SKColorToBrushConverter.cs | 29 + .../IColorGradientStorageProvider.cs | 28 + .../PropertyInput/PropertyInputViewModel.cs | 25 +- .../Artemis.UI.Shared/Styles/Artemis.axaml | 3 +- .../Artemis.UI.Shared/Styles/Condensed.axaml | 12 +- .../Styles/Controls/GradientPicker.axaml | 294 +++--- .../Controls/GradientPickerButton.axaml | 61 ++ .../ColorGradientPropertyInputView.axaml | 14 +- .../ColorGradientPropertyInputView.axaml.cs | 39 +- .../ColorGradientPropertyInputViewModel.cs | 72 +- .../SurfaceEditor/SurfaceDeviceViewModel.cs | 50 +- .../SurfaceEditor/SurfaceEditorView.axaml.cs | 4 +- .../SurfaceEditor/SurfaceEditorViewModel.cs | 9 +- .../Screens/Workshop/WorkshopView.axaml | 9 +- .../Screens/Workshop/WorkshopViewModel.cs | 21 +- 22 files changed, 1542 insertions(+), 585 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerButton.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Converters/SKColorToBrushConverter.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Providers/IColorGradientStorageProvider.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs index 75d7c8a61..fa786e7c3 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs @@ -20,7 +20,7 @@ namespace Artemis.Core private void CreateDataBindingRegistrations() { DataBinding.ClearDataBindingProperties(); - if (CurrentValue == null) + if (CurrentValue == null!) return; for (int index = 0; index < CurrentValue.Count; index++) @@ -54,10 +54,10 @@ namespace Artemis.Core private void OnCurrentValueSet(object? sender, LayerPropertyEventArgs e) { // Don't allow color gradients to be null - if (BaseValue == null) - BaseValue = DefaultValue ?? new ColorGradient(); + if (BaseValue == null!) + BaseValue = new ColorGradient(DefaultValue); - if (_subscribedGradient != BaseValue) + if (!ReferenceEquals(_subscribedGradient, BaseValue)) { if (_subscribedGradient != null) _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged; @@ -80,8 +80,8 @@ namespace Artemis.Core protected override void OnInitialize() { // Don't allow color gradients to be null - if (BaseValue == null) - BaseValue = DefaultValue ?? new ColorGradient(); + if (BaseValue == null!) + BaseValue = new ColorGradient(DefaultValue); base.OnInitialize(); } diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 589b7f5ba..173c1481e 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -6,379 +6,571 @@ using System.ComponentModel; using System.Linq; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A gradient containing a list of s +/// +public class ColorGradient : IList, IList, INotifyCollectionChanged { + #region Equality members + /// - /// A gradient containing a list of s + /// Determines whether all the stops in this gradient are equal to the stops in the given gradient. /// - public class ColorGradient : IList, IList, INotifyCollectionChanged + /// The other gradient to compare to + protected bool Equals(ColorGradient other) { - private static readonly SKColor[] FastLedRainbow = - { - new(0xFFFF0000), // Red - new(0xFFFF9900), // Orange - new(0xFFFFFF00), // Yellow - new(0xFF00FF00), // Green - new(0xFF00FF7E), // Aqua - new(0xFF0078FF), // Blue - new(0xFF9E22FF), // Purple - new(0xFFFF34AE), // Pink - new(0xFFFF0000) // and back to Red - }; + if (Count != other.Count) + return false; - private readonly List _stops; - - /// - /// Creates a new instance of the class - /// - public ColorGradient() + for (int i = 0; i < Count; i++) { - _stops = new List(); + if (!Equals(this[i], other[i])) + return false; } - /// - /// Gets all the colors in the color gradient - /// - /// The amount of times to repeat the colors - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - /// An array containing each color in the gradient - public SKColor[] GetColorsArray(int timesToRepeat = 0, bool seamless = false) + return true; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != this.GetType()) + return false; + return Equals((ColorGradient) obj); + } + + /// + public override int GetHashCode() + { + unchecked { - List result = new(); - if (timesToRepeat == 0) - { - result = this.Select(c => c.Color).ToList(); - } - else - { - List colors = this.Select(c => c.Color).ToList(); - for (int i = 0; i <= timesToRepeat; i++) - result.AddRange(colors); - } - - if (seamless && !IsSeamless()) - result.Add(result[0]); - - return result.ToArray(); - } - - /// - /// Gets all the positions in the color gradient - /// - /// - /// The amount of times to repeat the positions - /// - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - /// An array containing a position for each color between 0.0 and 1.0 - public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false) - { - List result = new(); - if (timesToRepeat == 0) - { - result = this.Select(c => c.Position).ToList(); - } - else - { - // Create stops and a list of divided stops - List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); - - // For each repeat cycle, add the base stops to the end result - for (int i = 0; i <= timesToRepeat; i++) - { - float lastStop = result.LastOrDefault(); - result.AddRange(stops.Select(s => s + lastStop)); - } - } - - if (seamless && !IsSeamless()) - { - // Compress current points evenly - float compression = 1f - 1f / result.Count; - for (int index = 0; index < result.Count; index++) - result[index] = result[index] * compression; - // Add one extra point at the end - result.Add(1f); - } - - return result.ToArray(); - } - - /// - /// Gets a color at any position between 0.0 and 1.0 using interpolation - /// - /// A position between 0.0 and 1.0 - public SKColor GetColor(float position) - { - if (!this.Any()) - return SKColor.Empty; - - ColorGradientStop[] stops = this.ToArray(); - if (position <= 0) return stops[0].Color; - if (position >= 1) return stops[^1].Color; - ColorGradientStop left = stops[0]; - ColorGradientStop? right = null; - foreach (ColorGradientStop stop in stops) - { - if (stop.Position >= position) - { - right = stop; - break; - } - - left = stop; - } - - if (right == null || left == right) - return left.Color; - - position = (float) Math.Round((position - left.Position) / (right.Position - left.Position), 2); - byte a = (byte) ((right.Color.Alpha - left.Color.Alpha) * position + left.Color.Alpha); - byte r = (byte) ((right.Color.Red - left.Color.Red) * position + left.Color.Red); - byte g = (byte) ((right.Color.Green - left.Color.Green) * position + left.Color.Green); - byte b = (byte) ((right.Color.Blue - left.Color.Blue) * position + left.Color.Blue); - return new SKColor(r, g, b, a); - } - - /// - /// Gets a new ColorGradient with colors looping through the HSV-spectrum - /// - /// - public static ColorGradient GetUnicornBarf() - { - ColorGradient gradient = new(); - for (int index = 0; index < FastLedRainbow.Length; index++) - { - SKColor skColor = FastLedRainbow[index]; - float position = 1f / (FastLedRainbow.Length - 1f) * index; - gradient.Add(new ColorGradientStop(skColor, position)); - } - - return gradient; - } - - /// - /// Determines whether the gradient is seamless - /// - /// if the gradient is seamless; otherwise - public bool IsSeamless() - { - return Count == 0 || this.First().Color.Equals(this.Last().Color); - } - - internal void Sort() - { - int requiredIndex = 0; - foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList()) - { - int actualIndex = _stops.IndexOf(colorGradientStop); - if (requiredIndex != actualIndex) - { - _stops.RemoveAt(actualIndex); - _stops.Insert(requiredIndex, colorGradientStop); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex)); - } - - requiredIndex++; - } - } - - private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - Sort(); - OnStopChanged(); - } - - #region Implementation of IEnumerable - - /// - public IEnumerator GetEnumerator() - { - return _stops.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - - #region Implementation of ICollection - - /// - public void Add(ColorGradientStop item) - { - _stops.Add(item); - item.PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _stops.IndexOf(item))); - } - - /// - public int Add(object? value) - { - if (value is ColorGradientStop stop) - _stops.Add(stop); - - return IndexOf(value); - } - - /// - public void Clear() - { - foreach (ColorGradientStop item in _stops) - item.PropertyChanged -= ItemOnPropertyChanged; - _stops.Clear(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - public bool Contains(object? value) - { - return _stops.Contains(value); - } - - /// - public int IndexOf(object? value) - { - return _stops.IndexOf(value); - } - - /// - public void Insert(int index, object? value) - { - if (value is ColorGradientStop stop) - _stops.Insert(index, stop); - } - - /// - public void Remove(object? value) - { - if (value is ColorGradientStop stop) - _stops.Remove(stop); - } - - /// - public bool Contains(ColorGradientStop item) - { - return _stops.Contains(item); - } - - /// - public void CopyTo(ColorGradientStop[] array, int arrayIndex) - { - _stops.CopyTo(array, arrayIndex); - } - - /// - public bool Remove(ColorGradientStop item) - { - item.PropertyChanged -= ItemOnPropertyChanged; - int index = _stops.IndexOf(item); - bool removed = _stops.Remove(item); - if (removed) - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); - - return removed; - } - - /// - public void CopyTo(Array array, int index) - { - _stops.CopyTo((ColorGradientStop[]) array, index); - } - - /// - public int Count => _stops.Count; - - /// - public bool IsSynchronized => false; - - /// - public object SyncRoot => this; - - /// - public bool IsReadOnly => false; - - object? IList.this[int index] - { - get => this[index]; - set => this[index] = (ColorGradientStop) value!; - } - - #endregion - - #region Implementation of IList - - /// - public int IndexOf(ColorGradientStop item) - { - return _stops.IndexOf(item); - } - - /// - public void Insert(int index, ColorGradientStop item) - { - _stops.Insert(index, item); - item.PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); - } - - /// - public void RemoveAt(int index) - { - _stops[index].PropertyChanged -= ItemOnPropertyChanged; - _stops.RemoveAt(index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); - } - - public bool IsFixedSize { get; } - - /// - public ColorGradientStop this[int index] - { - get => _stops[index]; - set - { - ColorGradientStop? oldValue = _stops[index]; - oldValue.PropertyChanged -= ItemOnPropertyChanged; - _stops[index] = value; - _stops[index].PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); - } - } - - #endregion - - #region Implementation of INotifyCollectionChanged - - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e); - } - - #endregion - - /// - /// Occurs when any of the stops has changed in some way - /// - public event EventHandler? StopChanged; - - private void OnStopChanged() - { - StopChanged?.Invoke(this, EventArgs.Empty); + int hash = 19; + foreach (ColorGradientStop stops in this) + hash = hash * 31 + stops.GetHashCode(); + return hash; } } + + #endregion + + private static readonly SKColor[] FastLedRainbow = + { + new(0xFFFF0000), // Red + new(0xFFFF9900), // Orange + new(0xFFFFFF00), // Yellow + new(0xFF00FF00), // Green + new(0xFF00FF7E), // Aqua + new(0xFF0078FF), // Blue + new(0xFF9E22FF), // Purple + new(0xFFFF34AE), // Pink + new(0xFFFF0000) // and back to Red + }; + + private readonly List _stops; + private bool _updating; + + /// + /// Creates a new instance of the class + /// + public ColorGradient() + { + _stops = new List(); + } + + /// + /// Creates a new instance of the class + /// + /// The color gradient to copy + public ColorGradient(ColorGradient? colorGradient) + { + _stops = new List(); + if (colorGradient == null) + return; + + foreach (ColorGradientStop colorGradientStop in colorGradient) + { + ColorGradientStop stop = new(colorGradientStop.Color, colorGradientStop.Position); + stop.PropertyChanged += ItemOnPropertyChanged; + _stops.Add(stop); + } + } + + /// + /// Gets all the colors in the color gradient + /// + /// The amount of times to repeat the colors + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + /// An array containing each color in the gradient + public SKColor[] GetColorsArray(int timesToRepeat = 0, bool seamless = false) + { + List result = new(); + if (timesToRepeat == 0) + { + result = this.Select(c => c.Color).ToList(); + } + else + { + List colors = this.Select(c => c.Color).ToList(); + for (int i = 0; i <= timesToRepeat; i++) + result.AddRange(colors); + } + + if (seamless && !IsSeamless()) + result.Add(result[0]); + + return result.ToArray(); + } + + /// + /// Gets all the positions in the color gradient + /// + /// + /// The amount of times to repeat the positions + /// + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + /// An array containing a position for each color between 0.0 and 1.0 + public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false) + { + List result = new(); + if (timesToRepeat == 0) + { + result = this.Select(c => c.Position).ToList(); + } + else + { + // Create stops and a list of divided stops + List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); + + // For each repeat cycle, add the base stops to the end result + for (int i = 0; i <= timesToRepeat; i++) + { + float lastStop = result.LastOrDefault(); + result.AddRange(stops.Select(s => s + lastStop)); + } + } + + if (seamless && !IsSeamless()) + { + // Compress current points evenly + float compression = 1f - 1f / result.Count; + for (int index = 0; index < result.Count; index++) + result[index] = result[index] * compression; + // Add one extra point at the end + result.Add(1f); + } + + return result.ToArray(); + } + + /// + /// Gets a color at any position between 0.0 and 1.0 using interpolation + /// + /// A position between 0.0 and 1.0 + public SKColor GetColor(float position) + { + if (!this.Any()) + return new SKColor(255, 255, 255); + + ColorGradientStop[] stops = this.ToArray(); + if (position <= 0) return stops[0].Color; + if (position >= 1) return stops[^1].Color; + ColorGradientStop left = stops[0]; + ColorGradientStop? right = null; + foreach (ColorGradientStop stop in stops) + { + if (stop.Position >= position) + { + right = stop; + break; + } + + left = stop; + } + + if (right == null || left == right) + return left.Color; + + position = (float) Math.Round((position - left.Position) / (right.Position - left.Position), 2); + byte a = (byte) ((right.Color.Alpha - left.Color.Alpha) * position + left.Color.Alpha); + byte r = (byte) ((right.Color.Red - left.Color.Red) * position + left.Color.Red); + byte g = (byte) ((right.Color.Green - left.Color.Green) * position + left.Color.Green); + byte b = (byte) ((right.Color.Blue - left.Color.Blue) * position + left.Color.Blue); + return new SKColor(r, g, b, a); + } + + /// + /// Gets a new ColorGradient with colors looping through the HSV-spectrum + /// + public static ColorGradient GetUnicornBarf() + { + ColorGradient gradient = new(); + for (int index = 0; index < FastLedRainbow.Length; index++) + { + SKColor skColor = FastLedRainbow[index]; + float position = 1f / (FastLedRainbow.Length - 1f) * index; + gradient.Add(new ColorGradientStop(skColor, position)); + } + + return gradient; + } + + /// + /// Gets a new ColorGradient with random colors from the HSV-spectrum + /// + /// The amount of stops to add + public ColorGradient GetRandom(int stops) + { + ColorGradient gradient = new(); + Random random = new(); + for (int index = 0; index < stops; index++) + { + SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100); + float position = 1f / (stops - 1f) * index; + gradient.Add(new ColorGradientStop(skColor, position)); + } + + return gradient; + } + + /// + /// Determines whether the gradient is seamless + /// + /// if the gradient is seamless; otherwise + public bool IsSeamless() + { + return Count == 0 || this.First().Color.Equals(this.Last().Color); + } + + /// + /// Spreads the color stops equally across the gradient. + /// + public void SpreadStops() + { + try + { + _updating = true; + for (int i = 0; i < Count; i++) + this[i].Position = MathF.Round(i / ((float) Count - 1), 3, MidpointRounding.AwayFromZero); + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// If not already the case, makes the gradient seamless by adding the first color to the end of the gradient and + /// compressing the other stops. + /// + /// If the gradient is already seamless, removes the last color and spreads the remaining stops to fill the freed + /// space. + /// + /// + public void ToggleSeamless() + { + try + { + _updating = true; + + if (IsSeamless()) + { + ColorGradientStop stopToRemove = this.Last(); + Remove(stopToRemove); + + // Uncompress the stops if there is still more than one + if (Count >= 2) + { + float multiplier = Count / (Count - 1f); + foreach (ColorGradientStop stop in this) + stop.Position = MathF.Round(Math.Min(stop.Position * multiplier, 100f), 3, MidpointRounding.AwayFromZero); + } + } + else + { + // Compress existing stops to the left + float multiplier = (Count - 1f) / Count; + foreach (ColorGradientStop stop in this) + stop.Position = MathF.Round(stop.Position * multiplier, 3, MidpointRounding.AwayFromZero); + + // Add a stop to the end that is the same color as the first stop + ColorGradientStop newStop = new(this.First().Color, 1f); + Add(newStop); + } + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Flips the stops of the gradient. + /// + public void FlipStops() + { + try + { + _updating = true; + foreach (ColorGradientStop stop in this) + stop.Position = 1 - stop.Position; + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Rotates the stops of the gradient shifting every stop over to the position of it's neighbor and wrapping around at + /// the end of the gradient. + /// + /// A boolean indicating whether or not the invert the rotation. + public void RotateStops(bool inverse) + { + try + { + _updating = true; + List stops = inverse + ? this.OrderBy(s => s.Position).ToList() + : this.OrderByDescending(s => s.Position).ToList(); + + float lastStopPosition = stops.Last().Position; + foreach (ColorGradientStop stop in stops) + (stop.Position, lastStopPosition) = (lastStopPosition, stop.Position); + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Occurs when any of the stops has changed in some way + /// + public event EventHandler? StopChanged; + + internal void Sort() + { + if (_updating) + return; + + int requiredIndex = 0; + foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList()) + { + int actualIndex = _stops.IndexOf(colorGradientStop); + if (requiredIndex != actualIndex) + { + _stops.RemoveAt(actualIndex); + _stops.Insert(requiredIndex, colorGradientStop); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex)); + } + + requiredIndex++; + } + } + + private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + Sort(); + OnStopChanged(); + } + + private void OnStopChanged() + { + StopChanged?.Invoke(this, EventArgs.Empty); + } + + #region Implementation of IEnumerable + + /// + public IEnumerator GetEnumerator() + { + return _stops.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Implementation of ICollection + + /// + public void Add(ColorGradientStop item) + { + _stops.Add(item); + item.PropertyChanged += ItemOnPropertyChanged; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + Sort(); + } + + /// + public int Add(object? value) + { + if (value is ColorGradientStop stop) + _stops.Add(stop); + + return IndexOf(value); + } + + /// + public void Clear() + { + foreach (ColorGradientStop item in _stops) + item.PropertyChanged -= ItemOnPropertyChanged; + _stops.Clear(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + public bool Contains(object? value) + { + return _stops.Contains(value); + } + + /// + public int IndexOf(object? value) + { + return _stops.IndexOf(value); + } + + /// + public void Insert(int index, object? value) + { + if (value is ColorGradientStop stop) + _stops.Insert(index, stop); + } + + /// + public void Remove(object? value) + { + if (value is ColorGradientStop stop) + _stops.Remove(stop); + } + + /// + public bool Contains(ColorGradientStop item) + { + return _stops.Contains(item); + } + + /// + public void CopyTo(ColorGradientStop[] array, int arrayIndex) + { + _stops.CopyTo(array, arrayIndex); + } + + /// + public bool Remove(ColorGradientStop item) + { + item.PropertyChanged -= ItemOnPropertyChanged; + int index = _stops.IndexOf(item); + bool removed = _stops.Remove(item); + if (removed) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + + return removed; + } + + /// + public void CopyTo(Array array, int index) + { + _stops.CopyTo((ColorGradientStop[]) array, index); + } + + /// + public int Count => _stops.Count; + + /// + public bool IsSynchronized => false; + + /// + public object SyncRoot => this; + + /// + public bool IsReadOnly => false; + + object? IList.this[int index] + { + get => this[index]; + set => this[index] = (ColorGradientStop) value!; + } + + #endregion + + #region Implementation of IList + + /// + public int IndexOf(ColorGradientStop item) + { + return _stops.IndexOf(item); + } + + /// + public void Insert(int index, ColorGradientStop item) + { + _stops.Insert(index, item); + item.PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + } + + /// + public void RemoveAt(int index) + { + _stops[index].PropertyChanged -= ItemOnPropertyChanged; + _stops.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); + } + + public bool IsFixedSize { get; } + + /// + public ColorGradientStop this[int index] + { + get => _stops[index]; + set + { + ColorGradientStop? oldValue = _stops[index]; + oldValue.PropertyChanged -= ItemOnPropertyChanged; + _stops[index] = value; + _stops[index].PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); + } + } + + #endregion + + #region Implementation of INotifyCollectionChanged + + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs index 242a883d3..8f19b2b95 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -1,4 +1,5 @@ -using SkiaSharp; +using System; +using SkiaSharp; namespace Artemis.Core { @@ -7,6 +8,34 @@ namespace Artemis.Core /// public class ColorGradientStop : CorePropertyChanged { + #region Equality members + + /// + protected bool Equals(ColorGradientStop other) + { + return _color.Equals(other._color) && _position.Equals(other._position); + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != this.GetType()) + return false; + return Equals((ColorGradientStop) obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(_color, _position); + } + + #endregion + private SKColor _color; private float _position; diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs b/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs new file mode 100644 index 000000000..ba4be29af --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs @@ -0,0 +1,25 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Artemis.UI.Shared.Controls.Flyouts; + +/// +/// Defines a flyout that hosts a gradient picker. +/// +public sealed class GradientPickerFlyout : Flyout +{ + private GradientPicker.GradientPicker? _picker; + + /// + /// Gets the gradient picker that this flyout hosts + /// + public GradientPicker.GradientPicker GradientPicker => _picker ??= new GradientPicker.GradientPicker(); + + /// + protected override Control CreatePresenter() + { + _picker ??= new GradientPicker.GradientPicker(); + FlyoutPresenter presenter = new() {Content = GradientPicker}; + return presenter; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs index 8359568f5..78dd6cf72 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs @@ -1,24 +1,33 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Windows.Input; using Artemis.Core; +using Artemis.UI.Shared.Providers; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Media; +using ReactiveUI; +using Button = Avalonia.Controls.Button; namespace Artemis.UI.Shared.Controls.GradientPicker; +/// +/// Represents a gradient picker that can be used to edit a gradient. +/// public class GradientPicker : TemplatedControl { - private LinearGradientBrush _linearGradientBrush = new(); - /// /// Gets or sets the color gradient. /// public static readonly StyledProperty ColorGradientProperty = - AvaloniaProperty.Register(nameof(Core.ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf()); + AvaloniaProperty.Register(nameof(ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf()); /// /// Gets or sets the currently selected color stop. @@ -27,11 +36,55 @@ public class GradientPicker : TemplatedControl AvaloniaProperty.Register(nameof(SelectedColorStop), defaultBindingMode: BindingMode.TwoWay); /// - /// Gets the linear gradient brush representing the color gradient. + /// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not. + /// + public static readonly StyledProperty IsCompactProperty = + AvaloniaProperty.Register(nameof(IsCompact), defaultBindingMode: BindingMode.TwoWay); + + /// + /// Gets or sets a storage provider to use for storing and loading gradients. + /// + public static readonly StyledProperty StorageProviderProperty = + AvaloniaProperty.Register(nameof(StorageProvider), notifying: StorageProviderChanged); + + /// + /// Gets the linear gradient brush representing the color gradient. /// public static readonly DirectProperty LinearGradientBrushProperty = AvaloniaProperty.RegisterDirect(nameof(LinearGradientBrush), g => g.LinearGradientBrush); + /// + /// Gets the command to execute when deleting stops. + /// + public static readonly DirectProperty DeleteStopProperty = + AvaloniaProperty.RegisterDirect(nameof(DeleteStop), g => g.DeleteStop); + + private readonly ICommand _deleteStop; + private Button? _flipStops; + private Border? _gradient; + private Button? _rotateStops; + private bool _shiftDown; + private Button? _spreadStops; + private Button? _toggleSeamless; + private ColorGradient? _lastColorGradient; + private ColorPicker? _colorPicker; + + public GradientPicker() + { + _deleteStop = ReactiveCommand.Create(s => + { + if (ColorGradient.Count <= 2) + return; + + int index = ColorGradient.IndexOf(s); + ColorGradient.Remove(s); + if (index > ColorGradient.Count - 1) + index--; + + SelectedColorStop = ColorGradient.ElementAtOrDefault(index); + }); + } + /// /// Gets or sets the color gradient. /// @@ -47,39 +100,130 @@ public class GradientPicker : TemplatedControl public ColorGradientStop? SelectedColorStop { get => GetValue(SelectedColorStopProperty); - set => SetValue(SelectedColorStopProperty, value); + set + { + if (_colorPicker != null && SelectedColorStop != null) + _colorPicker.PreviousColor = new Color2(SelectedColorStop.Color.Red, SelectedColorStop.Color.Green, SelectedColorStop.Color.Blue, SelectedColorStop.Color.Alpha); + SetValue(SelectedColorStopProperty, value); + } } /// - /// Gets the linear gradient brush representing the color gradient. + /// Gets or sets a boolean indicating whether the gradient picker should be in compact mode or not. /// - public LinearGradientBrush LinearGradientBrush + public bool IsCompact { - get => _linearGradientBrush; - private set => SetAndRaise(LinearGradientBrushProperty, ref _linearGradientBrush, value); + get => GetValue(IsCompactProperty); + set => SetValue(IsCompactProperty, value); + } + + /// + /// Gets or sets a storage provider to use for storing and loading gradients. + /// + public IColorGradientStorageProvider? StorageProvider + { + get => GetValue(StorageProviderProperty); + set => SetValue(StorageProviderProperty, value); + } + + /// + /// Gets the linear gradient brush representing the color gradient. + /// + public LinearGradientBrush LinearGradientBrush { get; } = new(); + + /// + /// Gets the command to execute when deleting stops. + /// + public ICommand DeleteStop + { + get => _deleteStop; + private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + if (_gradient != null) + _gradient.PointerPressed -= GradientOnPointerPressed; + if (_spreadStops != null) + _spreadStops.Click -= SpreadStopsOnClick; + if (_toggleSeamless != null) + _toggleSeamless.Click -= ToggleSeamlessOnClick; + if (_flipStops != null) + _flipStops.Click -= FlipStopsOnClick; + if (_rotateStops != null) + _rotateStops.Click -= RotateStopsOnClick; + + _colorPicker = e.NameScope.Find("ColorPicker"); + _gradient = e.NameScope.Find("Gradient"); + _spreadStops = e.NameScope.Find + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml new file mode 100644 index 000000000..f1b53611a --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml index b62cb245a..79b3a3fd2 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml @@ -2,7 +2,15 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared" + xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.DefaultTypes.PropertyInput.ColorGradientPropertyInputView"> - TODO - + x:Class="Artemis.UI.DefaultTypes.PropertyInput.ColorGradientPropertyInputView" + x:DataType="propertyInput:ColorGradientPropertyInputViewModel"> + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs index 26cf7391d..2288a4821 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.axaml.cs @@ -1,20 +1,29 @@ -using Avalonia; -using Avalonia.Controls; +using System; +using Artemis.UI.Shared.Controls.GradientPicker; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.DefaultTypes.PropertyInput -{ - public partial class ColorGradientPropertyInputView : ReactiveUserControl - { - public ColorGradientPropertyInputView() - { - InitializeComponent(); - } +namespace Artemis.UI.DefaultTypes.PropertyInput; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public class ColorGradientPropertyInputView : ReactiveUserControl +{ + public ColorGradientPropertyInputView() + { + InitializeComponent(); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void GradientPickerButton_OnFlyoutOpened(GradientPickerButton sender, EventArgs args) + { + ViewModel?.StartPreview(); + } + + private void GradientPickerButton_OnFlyoutClosed(GradientPickerButton sender, EventArgs args) + { + ViewModel?.ApplyPreview(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs index 262dd7117..0fe7783a7 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputViewModel.cs @@ -1,19 +1,87 @@ using System; +using System.Collections.Specialized; +using System.Linq; using Artemis.Core; using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.PropertyInput; +using Avalonia.Media; +using ReactiveUI; namespace Artemis.UI.DefaultTypes.PropertyInput; public class ColorGradientPropertyInputViewModel : PropertyInputViewModel { + private ColorGradient _colorGradient; + private ColorGradient? _originalGradient; + public ColorGradientPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) : base(layerProperty, profileEditorService, propertyInputService) { } - public void DialogClosed(object sender, EventArgs e) + public ColorGradient ColorGradient { - ApplyInputValue(); + get => _colorGradient; + set => this.RaiseAndSetIfChanged(ref _colorGradient, value); } + + protected override void OnInputValueChanged() + { + ColorGradient = new ColorGradient(InputValue); + } + + #region Overrides of PropertyInputViewModel + + /// + public override void StartPreview() + { + _originalGradient = LayerProperty.CurrentValue; + + // Set the property value to the gradient being edited by the picker, this will cause any updates to show right away because + // ColorGradient is a reference type + LayerProperty.CurrentValue = ColorGradient; + + // This won't fly if we ever support keyframes but at that point ColorGradient would have to be a value type anyway and this + // whole VM no longer makes sense + } + + /// + protected override void ApplyInputValue() + { + // Don't do anything, ColorGradient is a reference type and will update regardless + } + + /// + public override void ApplyPreview() + { + if (_originalGradient == null) + return; + + // Make sure something actually changed + if (Equals(ColorGradient, _originalGradient)) + { + LayerProperty.CurrentValue = _originalGradient; + } + else + { + // Update the gradient for realsies, giving the command a reference to the old gradient + ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, ColorGradient, _originalGradient, Time)); + } + + _originalGradient = null; + } + + /// + public override void DiscardPreview() + { + if (_originalGradient == null) + return; + + // Put the old gradient back + InputValue = _originalGradient; + ColorGradient = new ColorGradient(InputValue); + } + + #endregion } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs index 5dcf03e62..ee6f506f4 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceViewModel.cs @@ -28,7 +28,8 @@ namespace Artemis.UI.Screens.SurfaceEditor private double _dragOffsetY; private SelectionStatus _selectionStatus; - public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, IDeviceService deviceService, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, IWindowService windowService) + public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, IDeviceService deviceService, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, + IWindowService windowService) { _rgbService = rgbService; _deviceService = deviceService; @@ -77,29 +78,36 @@ namespace Artemis.UI.Screens.SurfaceEditor _dragOffsetY = Device.Y - mouseStartPosition.Y; } - public void UpdateMouseDrag(Point mousePosition) + public void UpdateMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { if (SelectionStatus != SelectionStatus.Selected) return; - float roundedX = (float) Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10f; - float roundedY = (float) Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10f; + float x = (float) (mousePosition.X + _dragOffsetX); + float y = (float) (mousePosition.Y + _dragOffsetY); - if (Fits(roundedX, roundedY)) + if (round) { - Device.X = roundedX; - Device.Y = roundedY; + x = (float) Math.Round(x / 10d, 0, MidpointRounding.AwayFromZero) * 10f; + y = (float) Math.Round(y / 10d, 0, MidpointRounding.AwayFromZero) * 10f; } - else if (Fits(roundedX, Device.Y)) + + + if (Fits(x, y, ignoreOverlap)) { - Device.X = roundedX; + Device.X = x; + Device.Y = y; } - else if (Fits(Device.X, roundedY)) + else if (Fits(x, Device.Y, ignoreOverlap)) { - Device.Y = roundedY; + Device.X = x; + } + else if (Fits(Device.X, y, ignoreOverlap)) + { + Device.Y = y; } } - + private void ExecuteIdentifyDevice(ArtemisDevice device) { _deviceService.IdentifyDevice(device); @@ -110,7 +118,7 @@ namespace Artemis.UI.Screens.SurfaceEditor await _windowService.ShowDialogAsync(_deviceVmFactory.DevicePropertiesViewModel(device)); } - private bool Fits(float x, float y) + private bool Fits(float x, float y, bool ignoreOverlap) { if (x < 0 || y < 0) return false; @@ -119,16 +127,16 @@ namespace Artemis.UI.Screens.SurfaceEditor if (x + Device.Rectangle.Width > maxTextureSize || y + Device.Rectangle.Height > maxTextureSize) return false; - List own = Device.Leds - .Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height)) - .ToList(); - List others = _rgbService.EnabledDevices + if (ignoreOverlap) + return true; + + IEnumerable own = Device.Leds + .Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height)); + IEnumerable others = _rgbService.EnabledDevices .Where(d => d != Device && d.IsEnabled) .SelectMany(d => d.Leds) - .Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height)) - .ToList(); - - + .Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height)); + return !own.Any(o => others.Any(l => l.IntersectsWith(o))); } } diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs index 696813b23..894b17306 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs @@ -73,7 +73,7 @@ namespace Artemis.UI.Screens.SurfaceEditor { if (!_dragging) ViewModel?.StartMouseDrag(e.GetPosition(_containerGrid)); - ViewModel?.UpdateMouseDrag(e.GetPosition(_containerGrid)); + ViewModel?.UpdateMouseDrag(e.GetPosition(_containerGrid), e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Alt)); } _dragging = true; @@ -93,7 +93,7 @@ namespace Artemis.UI.Screens.SurfaceEditor if (ReferenceEquals(e.Pointer.Captured, sender)) { - ViewModel?.StopMouseDrag(e.GetPosition(_containerGrid)); + ViewModel?.StopMouseDrag(e.GetPosition(_containerGrid), e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Alt)); e.Pointer.Capture(null); } } diff --git a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index e82817c31..8f8dc0f93 100644 --- a/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -61,23 +61,22 @@ namespace Artemis.UI.Screens.SurfaceEditor startedOn.SelectionStatus = SelectionStatus.Selected; foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels.Where(vm => vm != startedOn)) device.SelectionStatus = SelectionStatus.None; - } foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.StartMouseDrag(mousePosition); } - public void UpdateMouseDrag(Point mousePosition) + public void UpdateMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) - surfaceDeviceViewModel.UpdateMouseDrag(mousePosition); + surfaceDeviceViewModel.UpdateMouseDrag(mousePosition, round, ignoreOverlap); } - public void StopMouseDrag(Point mousePosition) + public void StopMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) - surfaceDeviceViewModel.UpdateMouseDrag(mousePosition); + surfaceDeviceViewModel.UpdateMouseDrag(mousePosition, round, ignoreOverlap); if (_saving) return; diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml index cff2680bd..1580a3f95 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -8,7 +8,7 @@ xmlns:attachedProperties="clr-namespace:Artemis.UI.Shared.AttachedProperties;assembly=Artemis.UI.Shared" xmlns:workshop="clr-namespace:Artemis.UI.Screens.Workshop" xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="800" x:Class="Artemis.UI.Screens.Workshop.WorkshopView" x:DataType="workshop:WorkshopViewModel"> @@ -47,7 +47,12 @@ - + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 97e951bf0..ca36e0e4c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -5,6 +5,7 @@ using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Input; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Screens.Workshop { @@ -13,6 +14,15 @@ namespace Artemis.UI.Screens.Workshop private readonly INotificationService _notificationService; private StandardCursorType _selectedCursor; private readonly ObservableAsPropertyHelper _cursor; + private ColorGradient _colorGradient = new() + { + new ColorGradientStop(new SKColor(0xFFFF6D00), 0f), + new ColorGradientStop(new SKColor(0xFFFE6806), 0.2f), + new ColorGradientStop(new SKColor(0xFFEF1788), 0.4f), + new ColorGradientStop(new SKColor(0xFFEF1788), 0.6f), + new ColorGradientStop(new SKColor(0xFF00FCCC), 0.8f), + new ColorGradientStop(new SKColor(0xFF00FCCC), 1f), + }; public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop") { @@ -33,7 +43,16 @@ namespace Artemis.UI.Screens.Workshop public Cursor Cursor => _cursor.Value; - public ColorGradient ColorGradient { get; set; } = ColorGradient.GetUnicornBarf(); + public ColorGradient ColorGradient + { + get => _colorGradient; + set => RaiseAndSetIfChanged(ref _colorGradient, value); + } + + public void CreateRandomGradient() + { + ColorGradient = ColorGradient.GetRandom(6); + } private void ExecuteShowNotification(NotificationSeverity severity) { From 8faa6b7a492788dd1637c8cd72e6c7c0d75d6b66 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 27 Feb 2022 23:06:58 +0100 Subject: [PATCH 139/270] Gradient picker - Added color randomization and condensed style --- .../Models/Profile/Colors/ColorGradient.cs | 126 ++++++++++-------- .../Controls/GradientPicker/GradientPicker.cs | 15 ++- .../Artemis.UI.Shared/Styles/Artemis.axaml | 11 +- .../Artemis.UI.Shared/Styles/Condensed.axaml | 4 + .../Styles/Controls/GradientPicker.axaml | 3 + .../Controls/GradientPickerButton.axaml | 16 ++- 6 files changed, 109 insertions(+), 66 deletions(-) diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 173c1481e..81854dda5 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -13,52 +13,6 @@ namespace Artemis.Core; /// public class ColorGradient : IList, IList, INotifyCollectionChanged { - #region Equality members - - /// - /// Determines whether all the stops in this gradient are equal to the stops in the given gradient. - /// - /// The other gradient to compare to - protected bool Equals(ColorGradient other) - { - if (Count != other.Count) - return false; - - for (int i = 0; i < Count; i++) - { - if (!Equals(this[i], other[i])) - return false; - } - - return true; - } - - /// - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != this.GetType()) - return false; - return Equals((ColorGradient) obj); - } - - /// - public override int GetHashCode() - { - unchecked - { - int hash = 19; - foreach (ColorGradientStop stops in this) - hash = hash * 31 + stops.GetHashCode(); - return hash; - } - } - - #endregion - private static readonly SKColor[] FastLedRainbow = { new(0xFFFF0000), // Red @@ -233,14 +187,7 @@ public class ColorGradient : IList, IList, INotifyCollectionC public ColorGradient GetRandom(int stops) { ColorGradient gradient = new(); - Random random = new(); - for (int index = 0; index < stops; index++) - { - SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100); - float position = 1f / (stops - 1f) * index; - gradient.Add(new ColorGradientStop(skColor, position)); - } - + gradient.Randomize(stops); return gradient; } @@ -360,6 +307,32 @@ public class ColorGradient : IList, IList, INotifyCollectionC } } + /// + /// Randomizes the color gradient with the given amount of . + /// + /// The amount of stops to put into the gradient. + public void Randomize(int stops) + { + try + { + _updating = true; + + Clear(); + Random random = new(); + for (int index = 0; index < stops; index++) + { + SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100); + float position = 1f / (stops - 1f) * index; + Add(new ColorGradientStop(skColor, position)); + } + } + finally + { + _updating = false; + Sort(); + } + } + /// /// Occurs when any of the stops has changed in some way /// @@ -396,6 +369,51 @@ public class ColorGradient : IList, IList, INotifyCollectionC StopChanged?.Invoke(this, EventArgs.Empty); } + #region Equality members + + /// + /// Determines whether all the stops in this gradient are equal to the stops in the given + /// gradient. + /// + /// The other gradient to compare to + protected bool Equals(ColorGradient other) + { + if (Count != other.Count) + return false; + + for (int i = 0; i < Count; i++) + if (!Equals(this[i], other[i])) + return false; + + return true; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((ColorGradient) obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hash = 19; + foreach (ColorGradientStop stops in this) + hash = hash * 31 + stops.GetHashCode(); + return hash; + } + } + + #endregion + #region Implementation of IEnumerable /// diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs index 78dd6cf72..05d2ca73e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs @@ -60,12 +60,13 @@ public class GradientPicker : TemplatedControl AvaloniaProperty.RegisterDirect(nameof(DeleteStop), g => g.DeleteStop); private readonly ICommand _deleteStop; + private bool _shiftDown; private Button? _flipStops; private Border? _gradient; private Button? _rotateStops; - private bool _shiftDown; private Button? _spreadStops; private Button? _toggleSeamless; + private Button? _randomize; private ColorGradient? _lastColorGradient; private ColorPicker? _colorPicker; @@ -153,6 +154,8 @@ public class GradientPicker : TemplatedControl _flipStops.Click -= FlipStopsOnClick; if (_rotateStops != null) _rotateStops.Click -= RotateStopsOnClick; + if (_randomize != null) + _randomize.Click -= RandomizeOnClick; _colorPicker = e.NameScope.Find("ColorPicker"); _gradient = e.NameScope.Find("Gradient"); @@ -160,6 +163,7 @@ public class GradientPicker : TemplatedControl _toggleSeamless = e.NameScope.Find + diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml index f1b53611a..498f01733 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Controls/GradientPickerButton.axaml @@ -21,7 +21,7 @@ - + \ No newline at end of file From fac7a618e78cc4adc8b46c3380566dc9aac75aaa Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 28 Feb 2022 23:26:18 +0100 Subject: [PATCH 140/270] Notifications - Fixed positioning in main window, Notifications - Added vertical stacking --- .../Controls/DeviceVisualizer.cs | 9 +- .../Services/Builders/NotificationBuilder.cs | 12 +-- .../Artemis.UI.Shared/Styles/Artemis.axaml | 3 +- .../Controls/GradientPickerButton.axaml | 58 ++++++----- .../Styles/Notifications.axaml | 27 ++++++ src/Avalonia/Artemis.UI/MainWindow.axaml | 17 ++-- .../Screens/Debugger/DebugView.axaml | 95 ++++++++++--------- .../Tabs/Render/RenderDebugView.axaml | 2 +- .../Screens/Device/DevicePropertiesView.axaml | 2 + .../Plugins/PluginSettingsWindowView.axaml | 1 + .../Properties/PropertyGroupViewModel.cs | 23 ++++- .../Panels/Properties/PropertyViewModel.cs | 19 +++- .../Screens/Workshop/WorkshopViewModel.cs | 3 +- 13 files changed, 178 insertions(+), 93 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Styles/Notifications.axaml diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 9358e1e61..b5a944faf 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -69,7 +69,14 @@ namespace Artemis.UI.Shared.Controls // Render device and LED images if (_deviceImage != null) - drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)); + { + drawingContext.DrawImage( + _deviceImage, + new Rect(_deviceImage.Size), + new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height), + RenderOptions.GetBitmapInterpolationMode(this) + ); + } if (!ShowColors) return; diff --git a/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs index 22fa90f18..ccf3c741f 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Builders/NotificationBuilder.cs @@ -26,12 +26,7 @@ public class NotificationBuilder public NotificationBuilder(Window parent) { _parent = parent; - _infoBar = new InfoBar - { - Classes = Classes.Parse("notification-info-bar"), - VerticalAlignment = VerticalAlignment.Bottom, - HorizontalAlignment = HorizontalAlignment.Right - }; + _infoBar = new InfoBar {Classes = Classes.Parse("notification-info-bar")}; } /// @@ -121,8 +116,9 @@ public class NotificationBuilder /// public Action Show() { - if (_parent.Content is not Panel panel) - throw new ArtemisSharedUIException("Can't display a notification on a window without a panel at its root."); + IPanel? panel = _parent.Find("NotificationContainer"); + if (panel == null) + throw new ArtemisSharedUIException("Can't display a notification on a window without a NotificationContainer."); Dispatcher.UIThread.Post(() => { diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index 2c44880a6..b3572739e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -35,9 +35,10 @@ + - + @@ -60,4 +67,11 @@ + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Notifications.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Notifications.axaml new file mode 100644 index 000000000..bfe195ede --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Notifications.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/MainWindow.axaml b/src/Avalonia/Artemis.UI/MainWindow.axaml index fada19202..f10f4287f 100644 --- a/src/Avalonia/Artemis.UI/MainWindow.axaml +++ b/src/Avalonia/Artemis.UI/MainWindow.axaml @@ -7,11 +7,14 @@ x:Class="Artemis.UI.MainWindow" Icon="/Assets/Images/Logo/application.ico" Title="Artemis 2.0"> - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml index 325cd5362..a6aac23e7 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/DebugView.axaml @@ -20,51 +20,56 @@ - - - - - - - Rendering - - - - - - - - Data Model - - - - - - - - Performance - - - - - - - - Logging - - - - - - - - - + + + + + + + + Rendering + + + + + + + + Data Model + + + + + + + + Performance + + + + + + + + Logging + + + + - + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml index e29e77355..cfb66a4e5 100644 --- a/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml @@ -24,7 +24,7 @@ - + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 7bfea49dc..f653027b5 100644 --- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -12,5 +12,6 @@ WindowStartupLocation="CenterOwner"> + diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs index 202d5f4df..edccd7921 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyGroupViewModel.cs @@ -12,11 +12,10 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes; using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.PropertyInput; -using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Properties; -public class PropertyGroupViewModel : ViewModelBase +public class PropertyGroupViewModel : ViewModelBase, IDisposable { private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly IPropertyInputService _propertyInputService; @@ -33,7 +32,7 @@ public class PropertyGroupViewModel : ViewModelBase TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this); TimelineGroupViewModel = layerPropertyVmFactory.TimelineGroupViewModel(this); - // TODO: Centralize visibility updating or do it here and dispose + LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged; _isVisible = !LayerPropertyGroup.IsHidden; PopulateChildren(); @@ -86,10 +85,12 @@ public class PropertyGroupViewModel : ViewModelBase return result; foreach (ViewModelBase child in Children) + { if (child is PropertyViewModel profileElementPropertyViewModel) result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels()); else if (child is PropertyGroupViewModel profileElementPropertyGroupViewModel) result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly)); + } return result; } @@ -120,4 +121,20 @@ public class PropertyGroupViewModel : ViewModelBase HasChildren = Children.Any(i => i is PropertyViewModel {IsVisible: true} || i is PropertyGroupViewModel {IsVisible: true}); } + + private void LayerPropertyGroupOnVisibilityChanged(object? sender, EventArgs e) + { + IsVisible = !LayerPropertyGroup.IsHidden; + } + + /// + public void Dispose() + { + LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged; + foreach (ViewModelBase viewModelBase in Children) + { + if (viewModelBase is IDisposable disposable) + disposable.Dispose(); + } + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs index 36cbbd55f..0dfa5ad45 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/Properties/PropertyViewModel.cs @@ -1,13 +1,13 @@ -using Artemis.Core; +using System; +using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Artemis.UI.Shared; -using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Properties; -public class PropertyViewModel : ViewModelBase +public class PropertyViewModel : ViewModelBase, IDisposable { private bool _isExpanded; private bool _isHighlighted; @@ -19,7 +19,7 @@ public class PropertyViewModel : ViewModelBase TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this); TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this); - // TODO: Centralize visibility updating or do it here and dispose + LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged; _isVisible = !LayerProperty.IsHidden; } @@ -44,4 +44,15 @@ public class PropertyViewModel : ViewModelBase get => _isExpanded; set => RaiseAndSetIfChanged(ref _isExpanded, value); } + + private void LayerPropertyOnVisibilityChanged(object? sender, LayerPropertyEventArgs e) + { + IsVisible = !LayerProperty.IsHidden; + } + + /// + public void Dispose() + { + LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged; + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index ca36e0e4c..cb7f09681 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -1,4 +1,5 @@ -using System.Reactive; +using System; +using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared.Services.Builders; From f5a902f5a5e18068bf9c58007753c26424339ede Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 6 Mar 2022 23:07:15 +0100 Subject: [PATCH 141/270] Profile tree - Drag & drop WIP --- src/Artemis.Core/Artemis.Core.csproj | 2 +- src/Artemis.Core/packages.lock.json | 6 +- src/Artemis.UI.Shared/packages.lock.json | 6 +- src/Artemis.UI/packages.lock.json | 6 +- .../packages.lock.json | 6 +- .../Artemis.UI.Linux/Artemis.UI.Linux.csproj | 10 +- .../Artemis.UI.Linux/packages.lock.json | 178 ++++----- .../Artemis.UI.MacOS/Artemis.UI.MacOS.csproj | 10 +- .../Artemis.UI.MacOS/packages.lock.json | 178 ++++----- .../Artemis.UI.Shared.csproj | 13 +- .../Commands/MoveProfileEElement.cs | 57 +++ .../Artemis.UI.Shared/packages.lock.json | 152 ++++---- .../Artemis.UI.Windows.csproj | 12 +- .../Artemis.UI.Windows/packages.lock.json | 180 +++++----- src/Avalonia/Artemis.UI/Artemis.UI.csproj | 21 +- .../Behaviors/SimpleContextDragBehavior.cs | 162 +++++++++ .../Behaviors/TreeItemDragBehavior.cs | 337 ++++++++++++++++++ .../Behaviors/ProfileTreeViewDropHandler.cs | 183 ++++++++++ .../ProfileTree/FolderTreeItemView.axaml.cs | 49 ++- .../ProfileTree/FolderTreeItemViewModel.cs | 8 +- .../ProfileTree/LayerTreeItemView.axaml.cs | 49 ++- .../ProfileTree/LayerTreeItemViewModel.cs | 7 + .../Panels/ProfileTree/ProfileTreeView.axaml | 40 ++- .../ProfileTree/ProfileTreeView.axaml.cs | 128 ++++++- .../ProfileTree/ProfileTreeViewModel.cs | 2 + .../Panels/ProfileTree/TreeItemViewModel.cs | 16 +- .../Screens/Sidebar/SidebarCategoryView.axaml | 3 +- src/Avalonia/Artemis.UI/packages.lock.json | 204 +++++------ 28 files changed, 1450 insertions(+), 575 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveProfileEElement.cs create mode 100644 src/Avalonia/Artemis.UI/Behaviors/SimpleContextDragBehavior.cs create mode 100644 src/Avalonia/Artemis.UI/Behaviors/TreeItemDragBehavior.cs create mode 100644 src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index fff601c10..5d0134f28 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -51,7 +51,7 @@ - + diff --git a/src/Artemis.Core/packages.lock.json b/src/Artemis.Core/packages.lock.json index 9e603e0af..2dec380d8 100644 --- a/src/Artemis.Core/packages.lock.json +++ b/src/Artemis.Core/packages.lock.json @@ -105,9 +105,9 @@ }, "Serilog.Sinks.Console": { "type": "Direct", - "requested": "[4.0.0, )", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index b5ae7d0c7..e164c6b35 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -402,8 +402,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -1341,7 +1341,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 58c5e7fe0..e822fc292 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -527,8 +527,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -1488,7 +1488,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", diff --git a/src/Artemis.VisualScripting/packages.lock.json b/src/Artemis.VisualScripting/packages.lock.json index 9ec35e3be..a3304ead0 100644 --- a/src/Artemis.VisualScripting/packages.lock.json +++ b/src/Artemis.VisualScripting/packages.lock.json @@ -383,8 +383,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -1346,7 +1346,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", diff --git a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 522d3d795..40039f1d5 100644 --- a/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -10,12 +10,12 @@ - - + + - - - + + + diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 9cda5ffc1..2ea08bb81 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -4,11 +4,11 @@ "net6.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "ftI5uGBFvWJpizGc6PT6lOb6FiO8AWcSYS9N4FWvXgOvuqWuTgmjwURPUkvajpeaQLKOOea6AbgotSyhV8NNoQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "7st8nMai1C1nqw1a2H+zXiVYTnnfFwZz7JGziEzJK4sF6+x/W77XkdcDgDHyihcK3clQZJexYr4f+PzK4BJhSQ==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,48 +19,48 @@ }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "wy4k1uarrmZJSJENCe1hjNpdCJWhup0gt6KA2TtZILfGG7imj+an5IuQZUSXtA7cl7A+6tF6lPQLo82gESUlXQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "v+siRNQYvSZR9lt/bBgb81t6LGbSC7pUo+APgPmKYGLeYcMij1O6CWk7tCh9hihMxNHYw/PEB06r8ZBQIg9YPg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Native": "0.10.12", - "Avalonia.Skia": "0.10.12", - "Avalonia.Win32": "0.10.12", - "Avalonia.X11": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.Native": "0.10.13", + "Avalonia.Skia": "0.10.13", + "Avalonia.Win32": "0.10.13", + "Avalonia.X11": "0.10.13" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "Pf9DGiSwl3+gPrRSHKFzDG20I9QJ5P1g6BexLKfHQH9+Cmax+a/UEVYQq4hGn0xhrmpuLYOeGHb8wasjAT4EfQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "stIGj0Rv/p/Re0GqlXCc061paifG6wT0YvrTUV/fQloNctW8Y4sf1xZNzr9dxdSz6+LG2AZjdZcSUhUGOCe6Zg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Controls.DataGrid": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Controls.DataGrid": "0.10.13", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "dOszpMtBKEACAFWtjwNibXMF2SBolJ3cV8ffDEOy2uuwjKBJqbSmHH+WSnui9KfbSF2igVpam4TqO6drJuEvjw==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "s5UUJ/MG97Jv9i+kxlgNSKx8Q6uJkgYMJ/LdOR3VM+7T32IRyhuxrNNCYygqk6avuJ4QqvMLU02T6v3GhI7WWA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.17, )", - "resolved": "17.1.17", - "contentHash": "0DLq44k4CVvfXcWHE4uigQa/wySOzxOTnWg50j2qZDpWzl9OP7QfIDJo39X3ffEjaVcCUFcbF9xAmm7fRX/q2g==", + "requested": "[17.1.50, )", + "resolved": "17.1.50", + "contentHash": "UofZH1WMwWNLvFkK2SH+gsYTkUmhFFJO0Pix9YG2RzdHQ92mRFCzHzPO1abeU8/cxzyc9hJHX7sBChzUj53Ulg==", "dependencies": { - "DynamicData": "7.4.9", - "Splat": "14.1.17" + "DynamicData": "7.5.2", + "Splat": "14.1.45" } }, "Avalonia.Angle.Windows.Natives": { @@ -70,11 +70,11 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "i3zM3P8PUY4FNhATZoFWkto3H66FcIrnJNMyOsl1fN0FPS6meysAwCKQwuou/oapyzZEODeAmCVdqB0AgjNHVw==", + "resolved": "0.10.13", + "contentHash": "51xcaYtJN41vX/4xUu8rNyoISTO4bdswpfZmTPjeBTdofrhZ6mzOqbxVk6tqT4gt88MPihbaPil4jsD4X4Aixw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } @@ -89,32 +89,32 @@ }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "j42uWCWkAfZchYPrdRccr4mjB0kppSby3TEMCuNrp9GcQi+JhEPEbBAohU7FpR4bkv5FF2KAlDX5WiG2T+04kg==", + "resolved": "0.10.13", + "contentHash": "/bKzscse/kY4RNMFk8r/jqLY/kS0fSkwp4TpqEF4UJeI8sHUvwe4yecnzNb1qDP9tCX4S6ML38LklAIqAk8gIQ==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "JnZc0zF7DcLcSX+SdnKQGzFa9mcKxawhTN8S3aiN8Eh3MZAKxa45LRrHFVTcHcy2jU4kOw+yPfONUmHpRcC0gw==", + "resolved": "0.10.13", + "contentHash": "pq3WiiOyFyhJHnYdxP/fOlcG9DfqhJ0W5CCfPX48QyOdODbPgMF5LY6BU+McDpeAJTwQ4LqVfznHZoCeHH12gg==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "ArrxniR8iShzMvXCS3vt5FXg9Fv3qK1UKzJwsSsY9iCuC8wKo2eevRj42qOhMCS98POTH5v8aUZBeoLlENa0vA==" + "resolved": "0.10.13", + "contentHash": "hnklCHyLCMrzWjMc3T0mYkRKdfUqpw2qCkf9HBRzyqnI6uG5tLw2QIlRF9zYC4BGOpx/B/647IcIjgq6H1ypzQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "3TGo8RLHaLqmU3chlyAqLkpw6vImfDMC30T18abpeYf1PIsNckRB+UFp12GDil9t/J9YB17zn4H6N+2plF4gZA==", + "resolved": "0.10.13", + "contentHash": "aG0JlUhCpwGM/QsN/+rRak7XlPy0Jtd5HaiCdYKtuBOc+ISGs6hmCJDKjklNANp9gZR/TUUgXkqk5VFMQUJkTA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "HarfBuzzSharp": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", @@ -136,10 +136,10 @@ }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "CnC65T8ScMK23BB+qJuiMicWQ5QIEiinnRzPqvAGUGyQbjIGpA5uOCKwzsOjUmzkhGqt31iDR0/Y3ZFbi5Mjog==", + "resolved": "0.10.13", + "contentHash": "CNUGWafAonBYYbHMliujDrs4JH2giH435GxU+O1q/nGyO5Mm+PXCG4NYsg+0zwp8yQBapFK7eYwzamU+re+cDw==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -147,37 +147,37 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "mUY1cF1p86/UgLl1cbSmY3nVIatKQsSCDOH4avssL07xmKlRfB2G7Gi8jlhWNkLJTLL7iQp/u3X6bv7bs+0zNQ==", + "resolved": "0.10.13", + "contentHash": "kXxn79KVB0ZfeZqQL7c2Dlvl96GBlRT8rzAh6g/j0hcgykQ55/e0be8Te6+Ny7hI+tFrob6lxvYdxYVUUCjHYg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.FreeDesktop": "0.10.12", - "Avalonia.Skia": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.FreeDesktop": "0.10.13", + "Avalonia.Skia": "0.10.13" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "upv7v1gZ31tCukw/KA1bB5+z29QuEWiZJ4KnW10daHlia1ru7q4lUJ/vCYKOy5L+zyi1MQg98SNYjRp5C64ZhQ==", + "resolved": "0.10.12.2", + "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "dependencies": { "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" + "Avalonia.Xaml.Interactions": "0.10.12.2", + "Avalonia.Xaml.Interactivity": "0.10.12.2" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "PSohbY4aQGiJVWfvLKkuUE71ZxvZ0/FuTc3Y5GJgTC41kCgeaiJTczkC2FjW5sZ8exPDabSp+ZukSsnm/z6y7A==", + "resolved": "0.10.12.2", + "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", "dependencies": { "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" + "Avalonia.Xaml.Interactivity": "0.10.12.2" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "uey4LjyIds78igMe7AZ072RI6GpO16sd6+6XF6LG0oE07De7ei0So14oOs4wLS4WJyaKDRSUK6PuhLaY1zIZdQ==", + "resolved": "0.10.12.2", + "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", "dependencies": { "Avalonia": "0.10.12" } @@ -201,8 +201,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.9", - "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", + "resolved": "7.5.4", + "contentHash": "1OpHPoyQGzHREiP6JXnPaBBx4KWVQZW7zBAZpKXc9kl4rcbEK4fo2/T3bDXZbHWKhDqVAISW9pE4Ug9+ms3RoA==", "dependencies": { "System.Reactive": "5.0.0" } @@ -222,12 +222,12 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "IH9eei7CrOUkUdxL2E/HZYKFgNupSVO+ju74CnVqmV7u7iolyz3g1cTHELqVgatEb+IqXw7KyeLr2459nUxYSw==", + "resolved": "1.3.0", + "contentHash": "xzcsuOswakMpz/EdA59NEOgaCtZ/9zsd5QWTB0YYQqSv1GF95Uk2aMVtO5gtfNrCT4lZvGNWVf3HGjYz9cHovQ==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", "MicroCom.CodeGenerator.MSBuild": "0.10.4", "MicroCom.Runtime": "0.10.4" } @@ -687,8 +687,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -758,16 +758,16 @@ }, "Splat": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" + "resolved": "14.1.45", + "contentHash": "ayHdfTUklD5ci0s9m4uYMccjtkKVjZ9fVPT5q3PN+SnvyD6bjQVRozOfUHwdwh4LAz9ETZjR/tAgrm+IapXKrw==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", + "resolved": "14.1.45", + "contentHash": "aU851Yb7i4kLzzrpo3KxFZg/U0vd36ORza9nk51pvL/QE+Jkm3ROqoPMf+BPfugEub2J1hHDEuLKJtxU7TAt0w==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.17" + "Splat": "14.1.45" } }, "Svg.Custom": { @@ -1735,7 +1735,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", @@ -1758,40 +1758,40 @@ "dependencies": { "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "1.0.0", - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Controls.PanAndZoom": "10.12.0", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", - "Avalonia.ReactiveUI": "0.10.12", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.2.1", + "Avalonia.Xaml.Behaviors": "0.10.12.2", + "Avalonia.Xaml.Interactions": "0.10.12.2", + "Avalonia.Xaml.Interactivity": "0.10.12.2", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", "RGB.NET.Layout": "1.0.0-prerelease7", - "ReactiveUI": "17.1.17", + "ReactiveUI": "17.1.50", "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178", - "Splat.Ninject": "14.1.17" + "Splat.Ninject": "14.1.45" } }, "artemis.ui.shared": { "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.12", - "Avalonia.ReactiveUI": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "Avalonia.Xaml.Behaviors": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12", - "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.2.1", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", - "ReactiveUI": "17.1.17", + "ReactiveUI": "17.1.50", "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178" } diff --git a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index 522d3d795..40039f1d5 100644 --- a/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Avalonia/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -10,12 +10,12 @@ - - + + - - - + + + diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 9cda5ffc1..2ea08bb81 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -4,11 +4,11 @@ "net6.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "ftI5uGBFvWJpizGc6PT6lOb6FiO8AWcSYS9N4FWvXgOvuqWuTgmjwURPUkvajpeaQLKOOea6AbgotSyhV8NNoQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "7st8nMai1C1nqw1a2H+zXiVYTnnfFwZz7JGziEzJK4sF6+x/W77XkdcDgDHyihcK3clQZJexYr4f+PzK4BJhSQ==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,48 +19,48 @@ }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "wy4k1uarrmZJSJENCe1hjNpdCJWhup0gt6KA2TtZILfGG7imj+an5IuQZUSXtA7cl7A+6tF6lPQLo82gESUlXQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "v+siRNQYvSZR9lt/bBgb81t6LGbSC7pUo+APgPmKYGLeYcMij1O6CWk7tCh9hihMxNHYw/PEB06r8ZBQIg9YPg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Native": "0.10.12", - "Avalonia.Skia": "0.10.12", - "Avalonia.Win32": "0.10.12", - "Avalonia.X11": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.Native": "0.10.13", + "Avalonia.Skia": "0.10.13", + "Avalonia.Win32": "0.10.13", + "Avalonia.X11": "0.10.13" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "Pf9DGiSwl3+gPrRSHKFzDG20I9QJ5P1g6BexLKfHQH9+Cmax+a/UEVYQq4hGn0xhrmpuLYOeGHb8wasjAT4EfQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "stIGj0Rv/p/Re0GqlXCc061paifG6wT0YvrTUV/fQloNctW8Y4sf1xZNzr9dxdSz6+LG2AZjdZcSUhUGOCe6Zg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Controls.DataGrid": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Controls.DataGrid": "0.10.13", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "dOszpMtBKEACAFWtjwNibXMF2SBolJ3cV8ffDEOy2uuwjKBJqbSmHH+WSnui9KfbSF2igVpam4TqO6drJuEvjw==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "s5UUJ/MG97Jv9i+kxlgNSKx8Q6uJkgYMJ/LdOR3VM+7T32IRyhuxrNNCYygqk6avuJ4QqvMLU02T6v3GhI7WWA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.17, )", - "resolved": "17.1.17", - "contentHash": "0DLq44k4CVvfXcWHE4uigQa/wySOzxOTnWg50j2qZDpWzl9OP7QfIDJo39X3ffEjaVcCUFcbF9xAmm7fRX/q2g==", + "requested": "[17.1.50, )", + "resolved": "17.1.50", + "contentHash": "UofZH1WMwWNLvFkK2SH+gsYTkUmhFFJO0Pix9YG2RzdHQ92mRFCzHzPO1abeU8/cxzyc9hJHX7sBChzUj53Ulg==", "dependencies": { - "DynamicData": "7.4.9", - "Splat": "14.1.17" + "DynamicData": "7.5.2", + "Splat": "14.1.45" } }, "Avalonia.Angle.Windows.Natives": { @@ -70,11 +70,11 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "i3zM3P8PUY4FNhATZoFWkto3H66FcIrnJNMyOsl1fN0FPS6meysAwCKQwuou/oapyzZEODeAmCVdqB0AgjNHVw==", + "resolved": "0.10.13", + "contentHash": "51xcaYtJN41vX/4xUu8rNyoISTO4bdswpfZmTPjeBTdofrhZ6mzOqbxVk6tqT4gt88MPihbaPil4jsD4X4Aixw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } @@ -89,32 +89,32 @@ }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "j42uWCWkAfZchYPrdRccr4mjB0kppSby3TEMCuNrp9GcQi+JhEPEbBAohU7FpR4bkv5FF2KAlDX5WiG2T+04kg==", + "resolved": "0.10.13", + "contentHash": "/bKzscse/kY4RNMFk8r/jqLY/kS0fSkwp4TpqEF4UJeI8sHUvwe4yecnzNb1qDP9tCX4S6ML38LklAIqAk8gIQ==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "JnZc0zF7DcLcSX+SdnKQGzFa9mcKxawhTN8S3aiN8Eh3MZAKxa45LRrHFVTcHcy2jU4kOw+yPfONUmHpRcC0gw==", + "resolved": "0.10.13", + "contentHash": "pq3WiiOyFyhJHnYdxP/fOlcG9DfqhJ0W5CCfPX48QyOdODbPgMF5LY6BU+McDpeAJTwQ4LqVfznHZoCeHH12gg==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "ArrxniR8iShzMvXCS3vt5FXg9Fv3qK1UKzJwsSsY9iCuC8wKo2eevRj42qOhMCS98POTH5v8aUZBeoLlENa0vA==" + "resolved": "0.10.13", + "contentHash": "hnklCHyLCMrzWjMc3T0mYkRKdfUqpw2qCkf9HBRzyqnI6uG5tLw2QIlRF9zYC4BGOpx/B/647IcIjgq6H1ypzQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "3TGo8RLHaLqmU3chlyAqLkpw6vImfDMC30T18abpeYf1PIsNckRB+UFp12GDil9t/J9YB17zn4H6N+2plF4gZA==", + "resolved": "0.10.13", + "contentHash": "aG0JlUhCpwGM/QsN/+rRak7XlPy0Jtd5HaiCdYKtuBOc+ISGs6hmCJDKjklNANp9gZR/TUUgXkqk5VFMQUJkTA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "HarfBuzzSharp": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", @@ -136,10 +136,10 @@ }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "CnC65T8ScMK23BB+qJuiMicWQ5QIEiinnRzPqvAGUGyQbjIGpA5uOCKwzsOjUmzkhGqt31iDR0/Y3ZFbi5Mjog==", + "resolved": "0.10.13", + "contentHash": "CNUGWafAonBYYbHMliujDrs4JH2giH435GxU+O1q/nGyO5Mm+PXCG4NYsg+0zwp8yQBapFK7eYwzamU+re+cDw==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -147,37 +147,37 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "mUY1cF1p86/UgLl1cbSmY3nVIatKQsSCDOH4avssL07xmKlRfB2G7Gi8jlhWNkLJTLL7iQp/u3X6bv7bs+0zNQ==", + "resolved": "0.10.13", + "contentHash": "kXxn79KVB0ZfeZqQL7c2Dlvl96GBlRT8rzAh6g/j0hcgykQ55/e0be8Te6+Ny7hI+tFrob6lxvYdxYVUUCjHYg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.FreeDesktop": "0.10.12", - "Avalonia.Skia": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.FreeDesktop": "0.10.13", + "Avalonia.Skia": "0.10.13" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "upv7v1gZ31tCukw/KA1bB5+z29QuEWiZJ4KnW10daHlia1ru7q4lUJ/vCYKOy5L+zyi1MQg98SNYjRp5C64ZhQ==", + "resolved": "0.10.12.2", + "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "dependencies": { "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" + "Avalonia.Xaml.Interactions": "0.10.12.2", + "Avalonia.Xaml.Interactivity": "0.10.12.2" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "PSohbY4aQGiJVWfvLKkuUE71ZxvZ0/FuTc3Y5GJgTC41kCgeaiJTczkC2FjW5sZ8exPDabSp+ZukSsnm/z6y7A==", + "resolved": "0.10.12.2", + "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", "dependencies": { "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" + "Avalonia.Xaml.Interactivity": "0.10.12.2" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "uey4LjyIds78igMe7AZ072RI6GpO16sd6+6XF6LG0oE07De7ei0So14oOs4wLS4WJyaKDRSUK6PuhLaY1zIZdQ==", + "resolved": "0.10.12.2", + "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", "dependencies": { "Avalonia": "0.10.12" } @@ -201,8 +201,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.9", - "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", + "resolved": "7.5.4", + "contentHash": "1OpHPoyQGzHREiP6JXnPaBBx4KWVQZW7zBAZpKXc9kl4rcbEK4fo2/T3bDXZbHWKhDqVAISW9pE4Ug9+ms3RoA==", "dependencies": { "System.Reactive": "5.0.0" } @@ -222,12 +222,12 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "IH9eei7CrOUkUdxL2E/HZYKFgNupSVO+ju74CnVqmV7u7iolyz3g1cTHELqVgatEb+IqXw7KyeLr2459nUxYSw==", + "resolved": "1.3.0", + "contentHash": "xzcsuOswakMpz/EdA59NEOgaCtZ/9zsd5QWTB0YYQqSv1GF95Uk2aMVtO5gtfNrCT4lZvGNWVf3HGjYz9cHovQ==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", "MicroCom.CodeGenerator.MSBuild": "0.10.4", "MicroCom.Runtime": "0.10.4" } @@ -687,8 +687,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -758,16 +758,16 @@ }, "Splat": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" + "resolved": "14.1.45", + "contentHash": "ayHdfTUklD5ci0s9m4uYMccjtkKVjZ9fVPT5q3PN+SnvyD6bjQVRozOfUHwdwh4LAz9ETZjR/tAgrm+IapXKrw==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", + "resolved": "14.1.45", + "contentHash": "aU851Yb7i4kLzzrpo3KxFZg/U0vd36ORza9nk51pvL/QE+Jkm3ROqoPMf+BPfugEub2J1hHDEuLKJtxU7TAt0w==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.17" + "Splat": "14.1.45" } }, "Svg.Custom": { @@ -1735,7 +1735,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", @@ -1758,40 +1758,40 @@ "dependencies": { "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "1.0.0", - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Controls.PanAndZoom": "10.12.0", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", - "Avalonia.ReactiveUI": "0.10.12", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.2.1", + "Avalonia.Xaml.Behaviors": "0.10.12.2", + "Avalonia.Xaml.Interactions": "0.10.12.2", + "Avalonia.Xaml.Interactivity": "0.10.12.2", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", "RGB.NET.Layout": "1.0.0-prerelease7", - "ReactiveUI": "17.1.17", + "ReactiveUI": "17.1.50", "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178", - "Splat.Ninject": "14.1.17" + "Splat.Ninject": "14.1.45" } }, "artemis.ui.shared": { "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.12", - "Avalonia.ReactiveUI": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "Avalonia.Xaml.Behaviors": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12", - "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.2.1", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", - "ReactiveUI": "17.1.17", + "ReactiveUI": "17.1.50", "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178" } diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index c47029bb7..e9e9bd769 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -17,16 +17,13 @@ - - + + - - - - - + + - + diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveProfileEElement.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveProfileEElement.cs new file mode 100644 index 000000000..69d67bd08 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/MoveProfileEElement.cs @@ -0,0 +1,57 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; + +/// +/// Represents a profile editor command that can be used to move a profile element. +/// +public class MoveProfileElement : IProfileEditorCommand +{ + private readonly int _originalIndex; + + private readonly ProfileElement? _originalParent; + private readonly ProfileElement _subject; + private readonly ProfileElement _target; + private readonly int _targetIndex; + + /// + /// Creates a new instance of the class. + /// + public MoveProfileElement(ProfileElement target, ProfileElement subject, int targetIndex) + { + _target = target; + _subject = subject; + _targetIndex = targetIndex; + + if (_subject.Parent != null) + { + _originalParent = _subject.Parent; + _originalIndex = _subject.Parent.Children.IndexOf(_subject); + } + + if (subject is Folder) + DisplayName = "Move folder"; + DisplayName = "Move layer"; + } + + #region Implementation of IProfileEditorCommand + + /// + public string DisplayName { get; } + + /// + public void Execute() + { + _subject.Parent?.RemoveChild(_subject); + _target.AddChild(_subject, _targetIndex); + } + + /// + public void Undo() + { + _target.RemoveChild(_subject); + _originalParent?.AddChild(_subject, _originalIndex); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index 7994e8935..cb93e25fb 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -4,11 +4,11 @@ "net6.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "ftI5uGBFvWJpizGc6PT6lOb6FiO8AWcSYS9N4FWvXgOvuqWuTgmjwURPUkvajpeaQLKOOea6AbgotSyhV8NNoQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "7st8nMai1C1nqw1a2H+zXiVYTnnfFwZz7JGziEzJK4sF6+x/W77XkdcDgDHyihcK3clQZJexYr4f+PzK4BJhSQ==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,11 +19,11 @@ }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "dOszpMtBKEACAFWtjwNibXMF2SBolJ3cV8ffDEOy2uuwjKBJqbSmHH+WSnui9KfbSF2igVpam4TqO6drJuEvjw==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "s5UUJ/MG97Jv9i+kxlgNSKx8Q6uJkgYMJ/LdOR3VM+7T32IRyhuxrNNCYygqk6avuJ4QqvMLU02T6v3GhI7WWA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } @@ -40,54 +40,24 @@ "Svg.Skia": "0.5.12" } }, - "Avalonia.Xaml.Behaviors": { - "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "upv7v1gZ31tCukw/KA1bB5+z29QuEWiZJ4KnW10daHlia1ru7q4lUJ/vCYKOy5L+zyi1MQg98SNYjRp5C64ZhQ==", - "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" - } - }, - "Avalonia.Xaml.Interactions": { - "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "PSohbY4aQGiJVWfvLKkuUE71ZxvZ0/FuTc3Y5GJgTC41kCgeaiJTczkC2FjW5sZ8exPDabSp+ZukSsnm/z6y7A==", - "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" - } - }, - "Avalonia.Xaml.Interactivity": { - "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "uey4LjyIds78igMe7AZ072RI6GpO16sd6+6XF6LG0oE07De7ei0So14oOs4wLS4WJyaKDRSUK6PuhLaY1zIZdQ==", - "dependencies": { - "Avalonia": "0.10.12" - } - }, "DynamicData": { "type": "Direct", - "requested": "[7.4.9, )", - "resolved": "7.4.9", - "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", + "requested": "[7.5.4, )", + "resolved": "7.5.4", + "contentHash": "1OpHPoyQGzHREiP6JXnPaBBx4KWVQZW7zBAZpKXc9kl4rcbEK4fo2/T3bDXZbHWKhDqVAISW9pE4Ug9+ms3RoA==", "dependencies": { "System.Reactive": "5.0.0" } }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.2.1, )", - "resolved": "1.2.1", - "contentHash": "IH9eei7CrOUkUdxL2E/HZYKFgNupSVO+ju74CnVqmV7u7iolyz3g1cTHELqVgatEb+IqXw7KyeLr2459nUxYSw==", + "requested": "[1.3.0, )", + "resolved": "1.3.0", + "contentHash": "xzcsuOswakMpz/EdA59NEOgaCtZ/9zsd5QWTB0YYQqSv1GF95Uk2aMVtO5gtfNrCT4lZvGNWVf3HGjYz9cHovQ==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", "MicroCom.CodeGenerator.MSBuild": "0.10.4", "MicroCom.Runtime": "0.10.4" } @@ -104,12 +74,12 @@ }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.17, )", - "resolved": "17.1.17", - "contentHash": "0DLq44k4CVvfXcWHE4uigQa/wySOzxOTnWg50j2qZDpWzl9OP7QfIDJo39X3ffEjaVcCUFcbF9xAmm7fRX/q2g==", + "requested": "[17.1.50, )", + "resolved": "17.1.50", + "contentHash": "UofZH1WMwWNLvFkK2SH+gsYTkUmhFFJO0Pix9YG2RzdHQ92mRFCzHzPO1abeU8/cxzyc9hJHX7sBChzUj53Ulg==", "dependencies": { - "DynamicData": "7.4.9", - "Splat": "14.1.17" + "DynamicData": "7.5.2", + "Splat": "14.1.45" } }, "ReactiveUI.Validation": { @@ -145,66 +115,66 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "i3zM3P8PUY4FNhATZoFWkto3H66FcIrnJNMyOsl1fN0FPS6meysAwCKQwuou/oapyzZEODeAmCVdqB0AgjNHVw==", + "resolved": "0.10.13", + "contentHash": "51xcaYtJN41vX/4xUu8rNyoISTO4bdswpfZmTPjeBTdofrhZ6mzOqbxVk6tqT4gt88MPihbaPil4jsD4X4Aixw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } }, "Avalonia.Desktop": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "wy4k1uarrmZJSJENCe1hjNpdCJWhup0gt6KA2TtZILfGG7imj+an5IuQZUSXtA7cl7A+6tF6lPQLo82gESUlXQ==", + "resolved": "0.10.13", + "contentHash": "v+siRNQYvSZR9lt/bBgb81t6LGbSC7pUo+APgPmKYGLeYcMij1O6CWk7tCh9hihMxNHYw/PEB06r8ZBQIg9YPg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Native": "0.10.12", - "Avalonia.Skia": "0.10.12", - "Avalonia.Win32": "0.10.12", - "Avalonia.X11": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.Native": "0.10.13", + "Avalonia.Skia": "0.10.13", + "Avalonia.Win32": "0.10.13", + "Avalonia.X11": "0.10.13" } }, "Avalonia.Diagnostics": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "Pf9DGiSwl3+gPrRSHKFzDG20I9QJ5P1g6BexLKfHQH9+Cmax+a/UEVYQq4hGn0xhrmpuLYOeGHb8wasjAT4EfQ==", + "resolved": "0.10.13", + "contentHash": "stIGj0Rv/p/Re0GqlXCc061paifG6wT0YvrTUV/fQloNctW8Y4sf1xZNzr9dxdSz6+LG2AZjdZcSUhUGOCe6Zg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Controls.DataGrid": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Controls.DataGrid": "0.10.13", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "j42uWCWkAfZchYPrdRccr4mjB0kppSby3TEMCuNrp9GcQi+JhEPEbBAohU7FpR4bkv5FF2KAlDX5WiG2T+04kg==", + "resolved": "0.10.13", + "contentHash": "/bKzscse/kY4RNMFk8r/jqLY/kS0fSkwp4TpqEF4UJeI8sHUvwe4yecnzNb1qDP9tCX4S6ML38LklAIqAk8gIQ==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "JnZc0zF7DcLcSX+SdnKQGzFa9mcKxawhTN8S3aiN8Eh3MZAKxa45LRrHFVTcHcy2jU4kOw+yPfONUmHpRcC0gw==", + "resolved": "0.10.13", + "contentHash": "pq3WiiOyFyhJHnYdxP/fOlcG9DfqhJ0W5CCfPX48QyOdODbPgMF5LY6BU+McDpeAJTwQ4LqVfznHZoCeHH12gg==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "ArrxniR8iShzMvXCS3vt5FXg9Fv3qK1UKzJwsSsY9iCuC8wKo2eevRj42qOhMCS98POTH5v8aUZBeoLlENa0vA==" + "resolved": "0.10.13", + "contentHash": "hnklCHyLCMrzWjMc3T0mYkRKdfUqpw2qCkf9HBRzyqnI6uG5tLw2QIlRF9zYC4BGOpx/B/647IcIjgq6H1ypzQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "3TGo8RLHaLqmU3chlyAqLkpw6vImfDMC30T18abpeYf1PIsNckRB+UFp12GDil9t/J9YB17zn4H6N+2plF4gZA==", + "resolved": "0.10.13", + "contentHash": "aG0JlUhCpwGM/QsN/+rRak7XlPy0Jtd5HaiCdYKtuBOc+ISGs6hmCJDKjklNANp9gZR/TUUgXkqk5VFMQUJkTA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "HarfBuzzSharp": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", @@ -215,10 +185,10 @@ }, "Avalonia.Win32": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "CnC65T8ScMK23BB+qJuiMicWQ5QIEiinnRzPqvAGUGyQbjIGpA5uOCKwzsOjUmzkhGqt31iDR0/Y3ZFbi5Mjog==", + "resolved": "0.10.13", + "contentHash": "CNUGWafAonBYYbHMliujDrs4JH2giH435GxU+O1q/nGyO5Mm+PXCG4NYsg+0zwp8yQBapFK7eYwzamU+re+cDw==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -226,12 +196,12 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "mUY1cF1p86/UgLl1cbSmY3nVIatKQsSCDOH4avssL07xmKlRfB2G7Gi8jlhWNkLJTLL7iQp/u3X6bv7bs+0zNQ==", + "resolved": "0.10.13", + "contentHash": "kXxn79KVB0ZfeZqQL7c2Dlvl96GBlRT8rzAh6g/j0hcgykQ55/e0be8Te6+Ny7hI+tFrob6lxvYdxYVUUCjHYg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.FreeDesktop": "0.10.12", - "Avalonia.Skia": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.FreeDesktop": "0.10.13", + "Avalonia.Skia": "0.10.13" } }, "Castle.Core": { @@ -674,8 +644,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -735,8 +705,8 @@ }, "Splat": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" + "resolved": "14.1.45", + "contentHash": "ayHdfTUklD5ci0s9m4uYMccjtkKVjZ9fVPT5q3PN+SnvyD6bjQVRozOfUHwdwh4LAz9ETZjR/tAgrm+IapXKrw==" }, "Svg.Custom": { "type": "Transitive", @@ -1703,7 +1673,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", diff --git a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 436ab8302..98d69b4cc 100644 --- a/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Avalonia/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -23,15 +23,15 @@ - - + + - - - + + + - + diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index 107f6cd1c..b7a683c5b 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -4,11 +4,11 @@ "net6.0-windows7.0": { "Avalonia": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "ftI5uGBFvWJpizGc6PT6lOb6FiO8AWcSYS9N4FWvXgOvuqWuTgmjwURPUkvajpeaQLKOOea6AbgotSyhV8NNoQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "7st8nMai1C1nqw1a2H+zXiVYTnnfFwZz7JGziEzJK4sF6+x/W77XkdcDgDHyihcK3clQZJexYr4f+PzK4BJhSQ==", "dependencies": { - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.ComponentModel.Annotations": "4.5.0", "System.Memory": "4.5.3", @@ -19,47 +19,47 @@ }, "Avalonia.Desktop": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "wy4k1uarrmZJSJENCe1hjNpdCJWhup0gt6KA2TtZILfGG7imj+an5IuQZUSXtA7cl7A+6tF6lPQLo82gESUlXQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "v+siRNQYvSZR9lt/bBgb81t6LGbSC7pUo+APgPmKYGLeYcMij1O6CWk7tCh9hihMxNHYw/PEB06r8ZBQIg9YPg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Native": "0.10.12", - "Avalonia.Skia": "0.10.12", - "Avalonia.Win32": "0.10.12", - "Avalonia.X11": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.Native": "0.10.13", + "Avalonia.Skia": "0.10.13", + "Avalonia.Win32": "0.10.13", + "Avalonia.X11": "0.10.13" } }, "Avalonia.Diagnostics": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "Pf9DGiSwl3+gPrRSHKFzDG20I9QJ5P1g6BexLKfHQH9+Cmax+a/UEVYQq4hGn0xhrmpuLYOeGHb8wasjAT4EfQ==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "stIGj0Rv/p/Re0GqlXCc061paifG6wT0YvrTUV/fQloNctW8Y4sf1xZNzr9dxdSz6+LG2AZjdZcSUhUGOCe6Zg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Controls.DataGrid": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Controls.DataGrid": "0.10.13", "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", "System.Reactive": "5.0.0" } }, "Avalonia.ReactiveUI": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "dOszpMtBKEACAFWtjwNibXMF2SBolJ3cV8ffDEOy2uuwjKBJqbSmHH+WSnui9KfbSF2igVpam4TqO6drJuEvjw==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "s5UUJ/MG97Jv9i+kxlgNSKx8Q6uJkgYMJ/LdOR3VM+7T32IRyhuxrNNCYygqk6avuJ4QqvMLU02T6v3GhI7WWA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "ReactiveUI": "13.2.10", "System.Reactive": "5.0.0" } }, "Avalonia.Win32": { "type": "Direct", - "requested": "[0.10.12, )", - "resolved": "0.10.12", - "contentHash": "CnC65T8ScMK23BB+qJuiMicWQ5QIEiinnRzPqvAGUGyQbjIGpA5uOCKwzsOjUmzkhGqt31iDR0/Y3ZFbi5Mjog==", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "CNUGWafAonBYYbHMliujDrs4JH2giH435GxU+O1q/nGyO5Mm+PXCG4NYsg+0zwp8yQBapFK7eYwzamU+re+cDw==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", "System.Drawing.Common": "4.5.0", "System.Numerics.Vectors": "4.5.0" @@ -82,12 +82,12 @@ }, "ReactiveUI": { "type": "Direct", - "requested": "[17.1.17, )", - "resolved": "17.1.17", - "contentHash": "0DLq44k4CVvfXcWHE4uigQa/wySOzxOTnWg50j2qZDpWzl9OP7QfIDJo39X3ffEjaVcCUFcbF9xAmm7fRX/q2g==", + "requested": "[17.1.50, )", + "resolved": "17.1.50", + "contentHash": "UofZH1WMwWNLvFkK2SH+gsYTkUmhFFJO0Pix9YG2RzdHQ92mRFCzHzPO1abeU8/cxzyc9hJHX7sBChzUj53Ulg==", "dependencies": { - "DynamicData": "7.4.9", - "Splat": "14.1.17" + "DynamicData": "7.5.2", + "Splat": "14.1.45" } }, "Avalonia.Angle.Windows.Natives": { @@ -97,11 +97,11 @@ }, "Avalonia.Controls.DataGrid": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "i3zM3P8PUY4FNhATZoFWkto3H66FcIrnJNMyOsl1fN0FPS6meysAwCKQwuou/oapyzZEODeAmCVdqB0AgjNHVw==", + "resolved": "0.10.13", + "contentHash": "51xcaYtJN41vX/4xUu8rNyoISTO4bdswpfZmTPjeBTdofrhZ6mzOqbxVk6tqT4gt88MPihbaPil4jsD4X4Aixw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Remote.Protocol": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Remote.Protocol": "0.10.13", "JetBrains.Annotations": "10.3.0", "System.Reactive": "5.0.0" } @@ -116,32 +116,32 @@ }, "Avalonia.FreeDesktop": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "j42uWCWkAfZchYPrdRccr4mjB0kppSby3TEMCuNrp9GcQi+JhEPEbBAohU7FpR4bkv5FF2KAlDX5WiG2T+04kg==", + "resolved": "0.10.13", + "contentHash": "/bKzscse/kY4RNMFk8r/jqLY/kS0fSkwp4TpqEF4UJeI8sHUvwe4yecnzNb1qDP9tCX4S6ML38LklAIqAk8gIQ==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Tmds.DBus": "0.9.0" } }, "Avalonia.Native": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "JnZc0zF7DcLcSX+SdnKQGzFa9mcKxawhTN8S3aiN8Eh3MZAKxa45LRrHFVTcHcy2jU4kOw+yPfONUmHpRcC0gw==", + "resolved": "0.10.13", + "contentHash": "pq3WiiOyFyhJHnYdxP/fOlcG9DfqhJ0W5CCfPX48QyOdODbPgMF5LY6BU+McDpeAJTwQ4LqVfznHZoCeHH12gg==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Avalonia.Remote.Protocol": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "ArrxniR8iShzMvXCS3vt5FXg9Fv3qK1UKzJwsSsY9iCuC8wKo2eevRj42qOhMCS98POTH5v8aUZBeoLlENa0vA==" + "resolved": "0.10.13", + "contentHash": "hnklCHyLCMrzWjMc3T0mYkRKdfUqpw2qCkf9HBRzyqnI6uG5tLw2QIlRF9zYC4BGOpx/B/647IcIjgq6H1ypzQ==" }, "Avalonia.Skia": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "3TGo8RLHaLqmU3chlyAqLkpw6vImfDMC30T18abpeYf1PIsNckRB+UFp12GDil9t/J9YB17zn4H6N+2plF4gZA==", + "resolved": "0.10.13", + "contentHash": "aG0JlUhCpwGM/QsN/+rRak7XlPy0Jtd5HaiCdYKtuBOc+ISGs6hmCJDKjklNANp9gZR/TUUgXkqk5VFMQUJkTA==", "dependencies": { - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "HarfBuzzSharp": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", @@ -163,37 +163,37 @@ }, "Avalonia.X11": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "mUY1cF1p86/UgLl1cbSmY3nVIatKQsSCDOH4avssL07xmKlRfB2G7Gi8jlhWNkLJTLL7iQp/u3X6bv7bs+0zNQ==", + "resolved": "0.10.13", + "contentHash": "kXxn79KVB0ZfeZqQL7c2Dlvl96GBlRT8rzAh6g/j0hcgykQ55/e0be8Te6+Ny7hI+tFrob6lxvYdxYVUUCjHYg==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.FreeDesktop": "0.10.12", - "Avalonia.Skia": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.FreeDesktop": "0.10.13", + "Avalonia.Skia": "0.10.13" } }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "upv7v1gZ31tCukw/KA1bB5+z29QuEWiZJ4KnW10daHlia1ru7q4lUJ/vCYKOy5L+zyi1MQg98SNYjRp5C64ZhQ==", + "resolved": "0.10.12.2", + "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "dependencies": { "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" + "Avalonia.Xaml.Interactions": "0.10.12.2", + "Avalonia.Xaml.Interactivity": "0.10.12.2" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "PSohbY4aQGiJVWfvLKkuUE71ZxvZ0/FuTc3Y5GJgTC41kCgeaiJTczkC2FjW5sZ8exPDabSp+ZukSsnm/z6y7A==", + "resolved": "0.10.12.2", + "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", "dependencies": { "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12" + "Avalonia.Xaml.Interactivity": "0.10.12.2" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.12", - "contentHash": "uey4LjyIds78igMe7AZ072RI6GpO16sd6+6XF6LG0oE07De7ei0So14oOs4wLS4WJyaKDRSUK6PuhLaY1zIZdQ==", + "resolved": "0.10.12.2", + "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", "dependencies": { "Avalonia": "0.10.12" } @@ -217,8 +217,8 @@ }, "DynamicData": { "type": "Transitive", - "resolved": "7.4.9", - "contentHash": "bzw9n1WgfflkhsScIaC7tzPlKFTJkfWVTOg2pjJjqzVqxF63ztaJ7HH306Iyx6bs+pC77fQbtE53UoPTpt+8dQ==", + "resolved": "7.5.4", + "contentHash": "1OpHPoyQGzHREiP6JXnPaBBx4KWVQZW7zBAZpKXc9kl4rcbEK4fo2/T3bDXZbHWKhDqVAISW9pE4Ug9+ms3RoA==", "dependencies": { "System.Reactive": "5.0.0" } @@ -238,12 +238,12 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.2.1", - "contentHash": "IH9eei7CrOUkUdxL2E/HZYKFgNupSVO+ju74CnVqmV7u7iolyz3g1cTHELqVgatEb+IqXw7KyeLr2459nUxYSw==", + "resolved": "1.3.0", + "contentHash": "xzcsuOswakMpz/EdA59NEOgaCtZ/9zsd5QWTB0YYQqSv1GF95Uk2aMVtO5gtfNrCT4lZvGNWVf3HGjYz9cHovQ==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", "MicroCom.CodeGenerator.MSBuild": "0.10.4", "MicroCom.Runtime": "0.10.4" } @@ -703,8 +703,8 @@ }, "Serilog.Sinks.Console": { "type": "Transitive", - "resolved": "4.0.0", - "contentHash": "yJQit9sTJ4xGLKgCujqDJsaGqBNJwGB/H898z+xYlMG06twy4//6LLnSrsmpduZxcHIG4im7cv+JmXLzXz2EkQ==", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", "dependencies": { "Serilog": "2.10.0" } @@ -774,16 +774,16 @@ }, "Splat": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "orBlJcQS4b1VZUlT+sJIensH0MsTYyCJlStT6bRwt71OFqNYD6V1SpkoIt6vKSf8YXgDT7QH/LuwWdLfTyHPrw==" + "resolved": "14.1.45", + "contentHash": "ayHdfTUklD5ci0s9m4uYMccjtkKVjZ9fVPT5q3PN+SnvyD6bjQVRozOfUHwdwh4LAz9ETZjR/tAgrm+IapXKrw==" }, "Splat.Ninject": { "type": "Transitive", - "resolved": "14.1.17", - "contentHash": "HPekZ89SfxHEX9NK+//w5JLBJmI8KLnUfh5yR/OVGTGRBSZVhHarAKgj/Ih3xJsqyl5L7l719C9RFo3qZBKn1A==", + "resolved": "14.1.45", + "contentHash": "aU851Yb7i4kLzzrpo3KxFZg/U0vd36ORza9nk51pvL/QE+Jkm3ROqoPMf+BPfugEub2J1hHDEuLKJtxU7TAt0w==", "dependencies": { "Ninject": "3.3.4", - "Splat": "14.1.17" + "Splat": "14.1.45" } }, "Svg.Custom": { @@ -1751,7 +1751,7 @@ "RGB.NET.Layout": "1.0.0-prerelease7", "RGB.NET.Presets": "1.0.0-prerelease7", "Serilog": "2.10.0", - "Serilog.Sinks.Console": "4.0.0", + "Serilog.Sinks.Console": "4.0.1", "Serilog.Sinks.Debug": "2.0.0", "Serilog.Sinks.File": "5.0.0", "SkiaSharp": "2.88.0-preview.178", @@ -1774,40 +1774,40 @@ "dependencies": { "Artemis.Core": "1.0.0", "Artemis.UI.Shared": "1.0.0", - "Avalonia": "0.10.12", + "Avalonia": "0.10.13", "Avalonia.Controls.PanAndZoom": "10.12.0", - "Avalonia.Desktop": "0.10.12", - "Avalonia.Diagnostics": "0.10.12", - "Avalonia.ReactiveUI": "0.10.12", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.2.1", + "Avalonia.Xaml.Behaviors": "0.10.12.2", + "Avalonia.Xaml.Interactions": "0.10.12.2", + "Avalonia.Xaml.Interactivity": "0.10.12.2", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", "Flurl.Http": "3.2.0", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", "RGB.NET.Layout": "1.0.0-prerelease7", - "ReactiveUI": "17.1.17", + "ReactiveUI": "17.1.50", "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178", - "Splat.Ninject": "14.1.17" + "Splat.Ninject": "14.1.45" } }, "artemis.ui.shared": { "type": "Project", "dependencies": { "Artemis.Core": "1.0.0", - "Avalonia": "0.10.12", - "Avalonia.ReactiveUI": "0.10.12", + "Avalonia": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "Avalonia.Xaml.Behaviors": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12", - "DynamicData": "7.4.9", - "FluentAvaloniaUI": "1.2.1", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease7", - "ReactiveUI": "17.1.17", + "ReactiveUI": "17.1.50", "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178" } diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 4395c30f9..d0eb58247 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -1,4 +1,4 @@ - + Library net6.0 @@ -15,23 +15,26 @@ - + - - - + + + - - + + + + + - + - + diff --git a/src/Avalonia/Artemis.UI/Behaviors/SimpleContextDragBehavior.cs b/src/Avalonia/Artemis.UI/Behaviors/SimpleContextDragBehavior.cs new file mode 100644 index 000000000..b89c2c65b --- /dev/null +++ b/src/Avalonia/Artemis.UI/Behaviors/SimpleContextDragBehavior.cs @@ -0,0 +1,162 @@ +// Based on AvaloniaBehaviors +// https://github.com/wieslawsoltes/AvaloniaBehaviors/blob/2f972ebb0066a2a4235126da7e103f684de1c777/src/Avalonia.Xaml.Interactions/DragAndDrop/ContextDragBehavior.cs + +// The MIT License (MIT) +// +// Copyright(c) Wiesław Šoltés +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +using System; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Core.Plugins; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Xaml.Interactions.DragAndDrop; +using Avalonia.Xaml.Interactivity; + +namespace Artemis.UI.Behaviors; + +public class SimpleContextDragBehavior : Behavior +{ + public static readonly StyledProperty ContextProperty = + AvaloniaProperty.Register(nameof(Context)); + + public static readonly StyledProperty HandlerProperty = + AvaloniaProperty.Register(nameof(Handler)); + + public static readonly StyledProperty HorizontalDragThresholdProperty = + AvaloniaProperty.Register(nameof(HorizontalDragThreshold), 3); + + public static readonly StyledProperty VerticalDragThresholdProperty = + AvaloniaProperty.Register(nameof(VerticalDragThreshold), 3); + + private Point _dragStartPoint; + private bool _lock; + private PointerEventArgs? _triggerEvent; + + public object? Context + { + get => GetValue(ContextProperty); + set => SetValue(ContextProperty, value); + } + + public IDragHandler? Handler + { + get => GetValue(HandlerProperty); + set => SetValue(HandlerProperty, value); + } + + public double HorizontalDragThreshold + { + get => GetValue(HorizontalDragThresholdProperty); + set => SetValue(HorizontalDragThresholdProperty, value); + } + + public double VerticalDragThreshold + { + get => GetValue(VerticalDragThresholdProperty); + set => SetValue(VerticalDragThresholdProperty, value); + } + + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject?.AddHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + AssociatedObject?.AddHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + AssociatedObject?.AddHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + } + + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject?.RemoveHandler(InputElement.PointerPressedEvent, AssociatedObject_PointerPressed); + AssociatedObject?.RemoveHandler(InputElement.PointerReleasedEvent, AssociatedObject_PointerReleased); + AssociatedObject?.RemoveHandler(InputElement.PointerMovedEvent, AssociatedObject_PointerMoved); + } + + private async Task DoDragDrop(PointerEventArgs triggerEvent, object? value) + { + DataObject data = new(); + data.Set(ContextDropBehavior.DataFormat, value!); + + await DragDrop.DoDragDrop(triggerEvent, data, DragDropEffects.Move); + } + + private void AssociatedObject_PointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPointProperties properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (!properties.IsLeftButtonPressed) + return; + if (e.Source is not IControl control || AssociatedObject?.DataContext != control.DataContext) + return; + + _dragStartPoint = e.GetPosition(null); + _triggerEvent = e; + _lock = true; + } + + private void AssociatedObject_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!Equals(e.Pointer.Captured, AssociatedObject)) + return; + + if (e.InitialPressMouseButton == MouseButton.Left && _triggerEvent is { }) + { + _triggerEvent = null; + _lock = false; + } + + e.Pointer.Capture(null); + } + + private async void AssociatedObject_PointerMoved(object? sender, PointerEventArgs e) + { + PointerPointProperties properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (!properties.IsLeftButtonPressed) + return; + + if (_triggerEvent is null) + return; + + Point point = e.GetPosition(null); + Point diff = _dragStartPoint - point; + double horizontalDragThreshold = HorizontalDragThreshold; + double verticalDragThreshold = VerticalDragThreshold; + + if (!(Math.Abs(diff.X) > horizontalDragThreshold) && !(Math.Abs(diff.Y) > verticalDragThreshold)) + return; + + e.Pointer.Capture(AssociatedObject); + + if (_lock) + _lock = false; + else + return; + + object? context = Context ?? AssociatedObject?.DataContext; + + Handler?.BeforeDragDrop(sender, _triggerEvent, context); + await DoDragDrop(_triggerEvent, context); + Handler?.AfterDragDrop(sender, _triggerEvent, context); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Behaviors/TreeItemDragBehavior.cs b/src/Avalonia/Artemis.UI/Behaviors/TreeItemDragBehavior.cs new file mode 100644 index 000000000..9fdb6f054 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Behaviors/TreeItemDragBehavior.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media.Transformation; +using Avalonia.Xaml.Interactivity; + +namespace Artemis.UI.Behaviors; + +/// +/// +public class TreeItemDragBehavior : Behavior +{ + /// + /// + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation)); + + /// + /// + public static readonly StyledProperty HorizontalDragThresholdProperty = + AvaloniaProperty.Register(nameof(HorizontalDragThreshold), 3); + + /// + /// + public static readonly StyledProperty VerticalDragThresholdProperty = + AvaloniaProperty.Register(nameof(VerticalDragThreshold), 3); + + private IControl? _draggedContainer; + private int _draggedIndex; + private bool _dragStarted; + private bool _enableDrag; + private ItemsControl? _itemsControl; + private Point _start; + private int _targetIndex; + + /// + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// + public double HorizontalDragThreshold + { + get => GetValue(HorizontalDragThresholdProperty); + set => SetValue(HorizontalDragThresholdProperty, value); + } + + /// + /// + public double VerticalDragThreshold + { + get => GetValue(VerticalDragThresholdProperty); + set => SetValue(VerticalDragThresholdProperty, value); + } + + /// + /// + protected override void OnAttached() + { + base.OnAttached(); + + if (AssociatedObject is { }) + { + AssociatedObject.AddHandler(InputElement.PointerReleasedEvent, Released, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerPressedEvent, Pressed, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerMovedEvent, Moved, RoutingStrategies.Tunnel); + AssociatedObject.AddHandler(InputElement.PointerCaptureLostEvent, CaptureLost, RoutingStrategies.Tunnel); + } + } + + /// + /// + protected override void OnDetaching() + { + base.OnDetaching(); + + if (AssociatedObject is { }) + { + AssociatedObject.RemoveHandler(InputElement.PointerReleasedEvent, Released); + AssociatedObject.RemoveHandler(InputElement.PointerPressedEvent, Pressed); + AssociatedObject.RemoveHandler(InputElement.PointerMovedEvent, Moved); + AssociatedObject.RemoveHandler(InputElement.PointerCaptureLostEvent, CaptureLost); + } + } + + private void Pressed(object? sender, PointerPressedEventArgs e) + { + PointerPointProperties properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (properties.IsLeftButtonPressed + && AssociatedObject?.Parent is ItemsControl itemsControl) + { + _enableDrag = true; + _dragStarted = false; + _start = e.GetPosition(AssociatedObject.Parent); + _draggedIndex = -1; + _targetIndex = -1; + _itemsControl = itemsControl; + _draggedContainer = AssociatedObject; + + if (_draggedContainer is { }) + SetDraggingPseudoClasses(_draggedContainer, true); + + AddTransforms(_itemsControl); + } + } + + private void Released(object? sender, PointerReleasedEventArgs e) + { + if (_dragStarted) + { + if (e.InitialPressMouseButton == MouseButton.Left) + Released(); + + e.Pointer.Capture(null); + } + } + + private void CaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + Released(); + } + + private void Released() + { + if (!_enableDrag) + return; + + RemoveTransforms(_itemsControl); + + if (_itemsControl is { }) + foreach (ItemContainerInfo? container in _itemsControl.ItemContainerGenerator.Containers) + SetDraggingPseudoClasses(container.ContainerControl, true); + + if (_dragStarted) + if (_draggedIndex >= 0 && _targetIndex >= 0 && _draggedIndex != _targetIndex) + MoveDraggedItem(_itemsControl, _draggedIndex, _targetIndex); + + if (_itemsControl is { }) + foreach (ItemContainerInfo? container in _itemsControl.ItemContainerGenerator.Containers) + SetDraggingPseudoClasses(container.ContainerControl, false); + + if (_draggedContainer is { }) + SetDraggingPseudoClasses(_draggedContainer, false); + + _draggedIndex = -1; + _targetIndex = -1; + _enableDrag = false; + _dragStarted = false; + _itemsControl = null; + + _draggedContainer = null; + } + + private void AddTransforms(ItemsControl? itemsControl) + { + if (itemsControl?.Items is null) + return; + + int i = 0; + + foreach (object? _ in itemsControl.Items) + { + IControl? container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i); + if (container is not null) + SetTranslateTransform(container, 0, 0); + + i++; + } + } + + private void RemoveTransforms(ItemsControl? itemsControl) + { + if (itemsControl?.Items is null) + return; + + int i = 0; + + foreach (object? _ in itemsControl.Items) + { + IControl? container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i); + if (container is not null) + SetTranslateTransform(container, 0, 0); + + i++; + } + } + + private void MoveDraggedItem(ItemsControl? itemsControl, int draggedIndex, int targetIndex) + { + if (itemsControl?.Items is not IList items) + return; + + object? draggedItem = items[draggedIndex]; + items.RemoveAt(draggedIndex); + items.Insert(targetIndex, draggedItem); + + if (itemsControl is SelectingItemsControl selectingItemsControl) + selectingItemsControl.SelectedIndex = targetIndex; + } + + private void Moved(object? sender, PointerEventArgs e) + { + PointerPointProperties properties = e.GetCurrentPoint(AssociatedObject).Properties; + if (properties.IsLeftButtonPressed) + { + if (_itemsControl?.Items is null || _draggedContainer?.RenderTransform is null || !_enableDrag) + return; + + Orientation orientation = Orientation; + Point position = e.GetPosition(_itemsControl); + double delta = orientation == Orientation.Horizontal ? position.X - _start.X : position.Y - _start.Y; + + if (!_dragStarted) + { + Point diff = _start - position; + double horizontalDragThreshold = HorizontalDragThreshold; + double verticalDragThreshold = VerticalDragThreshold; + + if (orientation == Orientation.Horizontal) + { + if (Math.Abs(diff.X) > horizontalDragThreshold) + _dragStarted = true; + else + return; + } + else + { + if (Math.Abs(diff.Y) > verticalDragThreshold) + _dragStarted = true; + else + return; + } + e.Pointer.Capture(AssociatedObject); + } + + if (orientation == Orientation.Horizontal) + SetTranslateTransform(_draggedContainer, delta, 0); + else + SetTranslateTransform(_draggedContainer, 0, delta); + + _draggedIndex = _itemsControl.ItemContainerGenerator.IndexFromContainer(_draggedContainer); + _targetIndex = -1; + + Rect draggedBounds = _draggedContainer.Bounds; + + double draggedStart = orientation == Orientation.Horizontal ? draggedBounds.X : draggedBounds.Y; + + double draggedDeltaStart = orientation == Orientation.Horizontal + ? draggedBounds.X + delta + : draggedBounds.Y + delta; + + double draggedDeltaEnd = orientation == Orientation.Horizontal + ? draggedBounds.X + delta + draggedBounds.Width + : draggedBounds.Y + delta + draggedBounds.Height; + + int i = 0; + + foreach (object? _ in _itemsControl.Items) + { + IControl? targetContainer = _itemsControl.ItemContainerGenerator.ContainerFromIndex(i); + if (targetContainer?.RenderTransform is null || ReferenceEquals(targetContainer, _draggedContainer)) + { + i++; + continue; + } + + // If the target container has children, there are two options + // Move into the top of the container + // Insert before or after a child in the container + + Rect targetBounds = targetContainer.Bounds; + double targetStart = orientation == Orientation.Horizontal ? targetBounds.X : targetBounds.Y; + + double targetMid = orientation == Orientation.Horizontal + ? targetBounds.X + targetBounds.Width / 2 + : targetBounds.Y + targetBounds.Height / 2; + + int targetIndex = _itemsControl.ItemContainerGenerator.IndexFromContainer(targetContainer); + + if (targetStart > draggedStart && draggedDeltaEnd >= targetMid) + { + if (orientation == Orientation.Horizontal) + SetTranslateTransform(targetContainer, -draggedBounds.Width, 0); + else + SetTranslateTransform(targetContainer, 0, -draggedBounds.Height); + + _targetIndex = _targetIndex == -1 ? targetIndex : + targetIndex > _targetIndex ? targetIndex : _targetIndex; + } + else if (targetStart < draggedStart && draggedDeltaStart <= targetMid) + { + if (orientation == Orientation.Horizontal) + SetTranslateTransform(targetContainer, draggedBounds.Width, 0); + else + SetTranslateTransform(targetContainer, 0, draggedBounds.Height); + + _targetIndex = _targetIndex == -1 ? targetIndex : + targetIndex < _targetIndex ? targetIndex : _targetIndex; + } + else + { + if (orientation == Orientation.Horizontal) + SetTranslateTransform(targetContainer, 0, 0); + else + SetTranslateTransform(targetContainer, 0, 0); + } + + i++; + } + } + } + + private void SetDraggingPseudoClasses(IControl control, bool isDragging) + { + if (isDragging) + ((IPseudoClasses) control.Classes).Add(":dragging"); + else + ((IPseudoClasses) control.Classes).Remove(":dragging"); + } + + private void SetTranslateTransform(IControl control, double x, double y) + { + TransformOperations.Builder transformBuilder = new(1); + transformBuilder.AppendTranslate(x, y); + control.RenderTransform = transformBuilder.Build(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs new file mode 100644 index 000000000..42eb776f6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.VisualTree; +using Avalonia.Xaml.Interactions.DragAndDrop; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Behaviors; + +public class ProfileTreeViewDropHandler : DropHandlerBase +{ + public override bool Validate(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) + { + if (e.Source is IControl && sender is TreeView treeView) + return Validate(treeView, e, sourceContext, targetContext, false); + + return false; + } + + public override bool Execute(object? sender, DragEventArgs e, object? sourceContext, object? targetContext, object? state) + { + bool result = false; + if (e.Source is IControl && sender is TreeView treeView) + result = Validate(treeView, e, sourceContext, targetContext, true); + + if (sender is ItemsControl itemsControl) + { + foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(itemsControl)) + SetDraggingPseudoClasses(treeViewItem, TreeDropType.After, false); + } + + return result; + } + + public override void Cancel(object? sender, RoutedEventArgs e) + { + if (e.Source is IControl control && control.FindAncestorOfType() != null) + SetDraggingPseudoClasses(control.FindAncestorOfType(), TreeDropType.After, false); + + base.Cancel(sender, e); + } + + private bool Validate(TreeView treeView, DragEventArgs e, object? sourceContext, object? targetContext, bool bExecute) where T : TreeItemViewModel + { + Point position = e.GetPosition(treeView); + IVisual? targetVisual = treeView.GetVisualAt(position).FindAncestorOfType(); + if (sourceContext is not T sourceNode || targetContext is not ProfileTreeViewModel vm || targetVisual is not IControl {DataContext: T targetNode}) + return false; + + TreeItemViewModel? sourceParent = sourceNode.Parent; + TreeItemViewModel? targetParent = targetNode.Parent; + ObservableCollection sourceNodes = sourceParent is { } ? sourceParent.Children : vm.Children; + ObservableCollection targetNodes = targetParent is { } ? targetParent.Children : vm.Children; + + int sourceIndex = sourceNodes.IndexOf(sourceNode); + int targetIndex = targetNodes.IndexOf(targetNode); + + // Update the target index according to the position + TreeDropType dropType = TreeDropType.Before; + if (!targetNode.SupportsChildren) + { + if (position.Y > targetVisual.Bounds.Top + targetVisual.Bounds.Height / 2) + dropType = TreeDropType.After; + } + else + { + IVisual? header = targetVisual.GetVisualDescendants().FirstOrDefault(d => d is Border b && b.Name == "PART_LayoutRoot"); + if (header != null) + { + double segments = header.Bounds.Height / 3.0; + if (position.Y > targetVisual.Bounds.Top + segments * 2) + dropType = TreeDropType.After; + else if (position.Y > targetVisual.Bounds.Top + segments) + dropType = TreeDropType.Into; + } + } + + if (dropType == TreeDropType.After) + targetIndex += 1; + else if (dropType == TreeDropType.Into) + targetIndex = 0; + + TreeItemViewModel? currentParent = targetNode.Parent; + while (currentParent != null) + { + if (currentParent == sourceNode) + return false; + currentParent = currentParent.Parent; + } + + if (sourceIndex < 0 || targetIndex < 0) + return false; + + if (e.DragEffects != DragDropEffects.Move) + return false; + + if (bExecute) + { + if (dropType == TreeDropType.Into) + { + targetNode.InsertElement(sourceNode, 0); + } + else + { + if (sourceParent == targetParent && sourceIndex < targetIndex) + targetIndex--; + if (targetNode.Parent != null) + targetNode.Parent.InsertElement(sourceNode, targetIndex); + } + } + else + { + SetDraggingPseudoClasses((IControl) targetVisual, dropType, true); + } + + return true; + } + + private List GetFlattenedTreeView(ItemsControl currentNode) + { + List result = new(); + + foreach (ItemContainerInfo containerInfo in currentNode.ItemContainerGenerator.Containers) + { + if (containerInfo.ContainerControl is TreeViewItem treeViewItem && containerInfo.Item is TreeItemViewModel viewModel) + { + result.Add(treeViewItem); + if (treeViewItem.ItemContainerGenerator.Containers.Any()) + result.AddRange(GetFlattenedTreeView(treeViewItem)); + } + } + + return result; + } + + private void SetDraggingPseudoClasses(IControl control, TreeDropType type, bool isDragging) + { + if (isDragging) + { + ((IPseudoClasses) control.Classes).Add(":dragging"); + if (type == TreeDropType.Before) + { + ((IPseudoClasses) control.Classes).Add(":dragging-before"); + ((IPseudoClasses) control.Classes).Remove(":dragging-after"); + ((IPseudoClasses) control.Classes).Remove(":dragging-into"); + } + else if (type == TreeDropType.After) + { + ((IPseudoClasses) control.Classes).Remove(":dragging-before"); + ((IPseudoClasses) control.Classes).Add(":dragging-after"); + ((IPseudoClasses) control.Classes).Remove(":dragging-into"); + } + else if (type == TreeDropType.Into) + { + ((IPseudoClasses) control.Classes).Remove(":dragging-before"); + ((IPseudoClasses) control.Classes).Remove(":dragging-after"); + ((IPseudoClasses) control.Classes).Add(":dragging-into"); + } + } + else + { + ((IPseudoClasses) control.Classes).Remove(":dragging"); + ((IPseudoClasses) control.Classes).Remove(":dragging-before"); + ((IPseudoClasses) control.Classes).Remove(":dragging-after"); + ((IPseudoClasses) control.Classes).Remove(":dragging-into"); + } + } + + private enum TreeDropType + { + Before, + After, + Into + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs index 6644b2451..29cbab812 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml.cs @@ -6,36 +6,35 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree; + +public class FolderTreeItemView : ReactiveUserControl { - public class FolderTreeItemView : ReactiveUserControl + public FolderTreeItemView() { - public FolderTreeItemView() - { - InitializeComponent(); - } + InitializeComponent(); + } - private void InitializeComponent() + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + this.WhenActivated(_ => ViewModel?.Rename.Subscribe(_ => { - AvaloniaXamlLoader.Load(this); - this.WhenActivated(_ => ViewModel?.Rename.Subscribe(_ => - { - this.Get("Input").Focus(); - this.Get("Input").SelectAll(); - })); - } + this.Get("Input").Focus(); + this.Get("Input").SelectAll(); + })); + } - private void InputElement_OnKeyUp(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - ViewModel?.SubmitRename(); - else if (e.Key == Key.Escape) - ViewModel?.CancelRename(); - } - - private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e) - { + private void InputElement_OnKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + ViewModel?.SubmitRename(); + else if (e.Key == Key.Escape) ViewModel?.CancelRename(); - } + } + + private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e) + { + ViewModel?.CancelRename(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index cbb530f99..2ae36aae5 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -16,6 +16,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree public Folder Folder { get; } - + + #region Overrides of TreeItemViewModel + + /// + public override bool SupportsChildren => true; + + #endregion } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs index b903451ae..69f80d702 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml.cs @@ -6,36 +6,35 @@ using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using ReactiveUI; -namespace Artemis.UI.Screens.ProfileEditor.ProfileTree +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree; + +public class LayerTreeItemView : ReactiveUserControl { - public class LayerTreeItemView : ReactiveUserControl + public LayerTreeItemView() { - public LayerTreeItemView() - { - InitializeComponent(); - } + InitializeComponent(); + } - private void InitializeComponent() + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + this.WhenActivated(_ => ViewModel?.Rename.Subscribe(_ => { - AvaloniaXamlLoader.Load(this); - this.WhenActivated(_ => ViewModel?.Rename.Subscribe(_ => - { - this.Get("Input").Focus(); - this.Get("Input").SelectAll(); - })); - } + this.Get("Input").Focus(); + this.Get("Input").SelectAll(); + })); + } - private void InputElement_OnKeyUp(object? sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - ViewModel?.SubmitRename(); - else if (e.Key == Key.Escape) - ViewModel?.CancelRename(); - } - - private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e) - { + private void InputElement_OnKeyUp(object? sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + ViewModel?.SubmitRename(); + else if (e.Key == Key.Escape) ViewModel?.CancelRename(); - } + } + + private void InputElement_OnLostFocus(object? sender, RoutedEventArgs e) + { + ViewModel?.CancelRename(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index ab577c3ce..e53af8eed 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -15,5 +15,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree } public Layer Layer { get; } + + #region Overrides of TreeItemViewModel + + /// + public override bool SupportsChildren => false; + + #endregion } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml index d2eb3db4e..f52ed381e 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml @@ -4,11 +4,49 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:profileTree="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree" + xmlns:profileBehaviors="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Behaviors" + xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.ProfileTreeView" x:DataType="profileTree:ProfileTreeViewModel"> + + + + + + + + + + - + @@ -111,7 +112,7 @@ - Date: Wed, 9 Mar 2022 00:10:41 +0100 Subject: [PATCH 142/270] Profile editor - Fixed new elements not rendering while paused Profile editor - Use default brush on new layers Profile tree - Implement drag & drop --- src/Artemis.Core/Models/Profile/Folder.cs | 4 +- .../Interfaces/ILayerBrushService.cs | 2 + .../Registration/LayerBrushService.cs | 8 + .../Commands/AddProfileElement.cs | 2 + .../PropertyInput/PropertyInputViewModel.cs | 2 +- .../Converters/ColorOpacityConverter.cs | 23 ++ .../SKColorPropertyInputView.axaml | 4 +- .../SKColorPropertyInputView.axaml.cs | 12 + .../Behaviors/ProfileTreeViewDropHandler.cs | 41 +- .../ProfileTree/FolderTreeItemViewModel.cs | 10 +- .../ProfileTree/LayerTreeItemViewModel.cs | 10 +- .../Panels/ProfileTree/ProfileTreeView.axaml | 50 ++- .../ProfileTree/ProfileTreeViewModel.cs | 12 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 384 +++++++++--------- 14 files changed, 335 insertions(+), 229 deletions(-) create mode 100644 src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 96dd20416..463ddfbb4 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -185,7 +185,7 @@ namespace Artemis.Core // No point rendering if all children are disabled if (!Children.Any(c => c is RenderProfileElement {Enabled: true})) return; - + SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; try { @@ -266,7 +266,7 @@ namespace Artemis.Core /// Occurs when a property affecting the rendering properties of this folder has been updated /// public event EventHandler? RenderPropertiesUpdated; - + /// protected override void Dispose(bool disposing) { diff --git a/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs b/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs index d0f6167e4..648495a55 100644 --- a/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs +++ b/src/Artemis.Core/Services/Registration/Interfaces/ILayerBrushService.cs @@ -27,5 +27,7 @@ namespace Artemis.Core.Services /// Returns the descriptor of the default layer brush /// LayerBrushDescriptor? GetDefaultLayerBrush(); + + void ApplyDefaultBrush(Layer layer); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Registration/LayerBrushService.cs b/src/Artemis.Core/Services/Registration/LayerBrushService.cs index c3bb68a59..0c3d7a998 100644 --- a/src/Artemis.Core/Services/Registration/LayerBrushService.cs +++ b/src/Artemis.Core/Services/Registration/LayerBrushService.cs @@ -46,5 +46,13 @@ namespace Artemis.Core.Services defaultReference.Value.BrushType ??= "SolidBrush"; return LayerBrushStore.Get(defaultReference.Value.LayerBrushProviderId, defaultReference.Value.BrushType)?.LayerBrushDescriptor; } + + /// + public void ApplyDefaultBrush(Layer layer) + { + LayerBrushDescriptor? brush = GetDefaultLayerBrush(); + if (brush != null) + layer.ChangeLayerBrush(brush.CreateInstance(layer, null)); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs index eab84bd7f..a7dad90e7 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/AddProfileElement.cs @@ -47,6 +47,8 @@ public class AddProfileElement : IProfileEditorCommand, IDisposable { _isAdded = true; _target.AddChild(_subject, _index); + + _subject.Enable(); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs index 469758d88..742675c7e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs @@ -134,7 +134,7 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel return; if (_preview.DiscardPreview() && _preview.PreviewValue != null) - ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _preview.PreviewValue, Time)); + ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _preview.OriginalValue, Time)); _preview = null; } diff --git a/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs b/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs new file mode 100644 index 000000000..e69ed58f5 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Converters/ColorOpacityConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Artemis.UI.Converters; + +public class ColorOpacityConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is Color color && double.TryParse(parameter?.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double multiplier)) + return new Color((byte) (color.A * multiplier), color.R, color.G, color.B); + return value; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return value; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml index a46f7fa29..eadb2ff57 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml @@ -55,7 +55,9 @@ + ShowAcceptDismissButtons="False" + FlyoutOpened="ColorPickerButton_OnFlyoutOpened" + FlyoutClosed="ColorPickerButton_OnFlyoutClosed" /> \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs index 13922a4fc..8240df3f0 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs @@ -1,8 +1,10 @@ +using System; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using FluentAvalonia.UI.Controls; namespace Artemis.UI.DefaultTypes.PropertyInput { @@ -17,5 +19,15 @@ namespace Artemis.UI.DefaultTypes.PropertyInput { AvaloniaXamlLoader.Load(this); } + + private void ColorPickerButton_OnFlyoutOpened(ColorPickerButton sender, EventArgs args) + { + ViewModel?.StartPreview(); + } + + private void ColorPickerButton_OnFlyoutClosed(ColorPickerButton sender, EventArgs args) + { + ViewModel?.ApplyPreview(); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs index 42eb776f6..7958869d9 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Behaviors/ProfileTreeViewDropHandler.cs @@ -1,15 +1,11 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Generators; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Media; using Avalonia.VisualTree; using Avalonia.Xaml.Interactions.DragAndDrop; @@ -34,7 +30,7 @@ public class ProfileTreeViewDropHandler : DropHandlerBase if (sender is ItemsControl itemsControl) { foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(itemsControl)) - SetDraggingPseudoClasses(treeViewItem, TreeDropType.After, false); + SetDraggingPseudoClasses(treeViewItem, TreeDropType.None); } return result; @@ -42,8 +38,11 @@ public class ProfileTreeViewDropHandler : DropHandlerBase public override void Cancel(object? sender, RoutedEventArgs e) { - if (e.Source is IControl control && control.FindAncestorOfType() != null) - SetDraggingPseudoClasses(control.FindAncestorOfType(), TreeDropType.After, false); + if (sender is ItemsControl itemsControl) + { + foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(itemsControl)) + SetDraggingPseudoClasses(treeViewItem, TreeDropType.None); + } base.Cancel(sender, e); } @@ -102,6 +101,9 @@ public class ProfileTreeViewDropHandler : DropHandlerBase if (e.DragEffects != DragDropEffects.Move) return false; + foreach (TreeViewItem treeViewItem in GetFlattenedTreeView(treeView)) + SetDraggingPseudoClasses(treeViewItem, TreeDropType.None); + if (bExecute) { if (dropType == TreeDropType.Into) @@ -118,7 +120,7 @@ public class ProfileTreeViewDropHandler : DropHandlerBase } else { - SetDraggingPseudoClasses((IControl) targetVisual, dropType, true); + SetDraggingPseudoClasses((IControl) targetVisual, dropType); } return true; @@ -130,7 +132,7 @@ public class ProfileTreeViewDropHandler : DropHandlerBase foreach (ItemContainerInfo containerInfo in currentNode.ItemContainerGenerator.Containers) { - if (containerInfo.ContainerControl is TreeViewItem treeViewItem && containerInfo.Item is TreeItemViewModel viewModel) + if (containerInfo.ContainerControl is TreeViewItem treeViewItem && containerInfo.Item is TreeItemViewModel) { result.Add(treeViewItem); if (treeViewItem.ItemContainerGenerator.Containers.Any()) @@ -141,9 +143,16 @@ public class ProfileTreeViewDropHandler : DropHandlerBase return result; } - private void SetDraggingPseudoClasses(IControl control, TreeDropType type, bool isDragging) + private void SetDraggingPseudoClasses(IControl control, TreeDropType type) { - if (isDragging) + if (type == TreeDropType.None) + { + ((IPseudoClasses) control.Classes).Remove(":dragging"); + ((IPseudoClasses) control.Classes).Remove(":dragging-before"); + ((IPseudoClasses) control.Classes).Remove(":dragging-after"); + ((IPseudoClasses) control.Classes).Remove(":dragging-into"); + } + else { ((IPseudoClasses) control.Classes).Add(":dragging"); if (type == TreeDropType.Before) @@ -165,19 +174,13 @@ public class ProfileTreeViewDropHandler : DropHandlerBase ((IPseudoClasses) control.Classes).Add(":dragging-into"); } } - else - { - ((IPseudoClasses) control.Classes).Remove(":dragging"); - ((IPseudoClasses) control.Classes).Remove(":dragging-before"); - ((IPseudoClasses) control.Classes).Remove(":dragging-after"); - ((IPseudoClasses) control.Classes).Remove(":dragging-into"); - } } private enum TreeDropType { Before, After, - Into + Into, + None } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs index 2ae36aae5..e8e3f3558 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemViewModel.cs @@ -8,8 +8,14 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public class FolderTreeItemViewModel : TreeItemViewModel { - public FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder, IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory, IRgbService rgbService) - : base(parent, folder, windowService, profileEditorService, rgbService, profileEditorVmFactory) + public FolderTreeItemViewModel(TreeItemViewModel? parent, + Folder folder, + IWindowService windowService, + IProfileEditorService profileEditorService, + ILayerBrushService layerBrushService, + IProfileEditorVmFactory profileEditorVmFactory, + IRgbService rgbService) + : base(parent, folder, windowService, profileEditorService, rgbService, layerBrushService, profileEditorVmFactory) { Folder = folder; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs index e53af8eed..918339c72 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemViewModel.cs @@ -8,8 +8,14 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public class LayerTreeItemViewModel : TreeItemViewModel { - public LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer, IWindowService windowService, IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory, IRgbService rgbService) - : base(parent, layer, windowService, profileEditorService, rgbService, profileEditorVmFactory) + public LayerTreeItemViewModel(TreeItemViewModel? parent, + Layer layer, + IWindowService windowService, + IProfileEditorService profileEditorService, + IRgbService rgbService, + ILayerBrushService layerBrushService, + IProfileEditorVmFactory profileEditorVmFactory) + : base(parent, layer, windowService, profileEditorService, rgbService, layerBrushService, profileEditorVmFactory) { Layer = layer; } diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml index f52ed381e..d3a5cb1e8 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeView.axaml @@ -6,14 +6,18 @@ xmlns:profileTree="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree" xmlns:profileBehaviors="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.Behaviors" xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.ProfileTreeView" x:DataType="profileTree:ProfileTreeViewModel"> + + + - + - + @@ -99,20 +98,7 @@ - - - - - - - - - - - (("profileCategory", ProfileCategory), ("profileConfiguration", null)); - if (result) - _sidebarViewModel.UpdateProfileCategories(); + ProfileConfiguration? result = await _windowService.ShowDialogAsync( + ("profileCategory", ProfileCategory), + ("profileConfiguration", null) + ); + if (result != null) + { + SidebarProfileConfigurationViewModel viewModel = _vmFactory.SidebarProfileConfigurationViewModel(_sidebarViewModel, result); + ProfileConfigurations.Add(viewModel); + SelectedProfileConfiguration = viewModel; + } } private void CreateProfileViewModels() diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml index 42c77832e..a803394db 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml @@ -102,7 +102,7 @@ - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index 7d8e70086..9244d4791 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -1,20 +1,36 @@ +using System; using Avalonia; -using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.VisualScripting -{ - public partial class NodeView : ReactiveUserControl - { - public NodeView() - { - InitializeComponent(); - } +namespace Artemis.UI.Screens.VisualScripting; - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } +public class NodeView : ReactiveUserControl +{ + public NodeView() + { + InitializeComponent(); } -} + + #region Overrides of Layoutable + + /// + protected override Size MeasureOverride(Size availableSize) + { + // Take the base implementation's size + (double width, double height) = base.MeasureOverride(availableSize); + + // Ceil the resulting size + width = Math.Ceiling(width / 10.0) * 10.0; + height = Math.Ceiling(height / 10.0) * 10.0; + + return new Size(width, height); + } + + #endregion + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index e38eed9ed..07be3a158 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -1,23 +1,79 @@ -using Artemis.Core; +using System; +using System.Collections.ObjectModel; +using System.Reactive; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; -using Avalonia; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Avalonia.Controls.Mixins; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class NodeViewModel : ActivatableViewModelBase { - private Point _position; + private readonly NodeScript _nodeScript; + private readonly INodeEditorService _nodeEditorService; - public NodeViewModel(INode node) + private ICustomNodeViewModel? _customNodeViewModel; + private ReactiveCommand? _deleteNode; + private ObservableAsPropertyHelper? _isStaticNode; + + public NodeViewModel(NodeScript nodeScript, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { + _nodeScript = nodeScript; + _nodeEditorService = nodeEditorService; Node = node; + + SourceList nodePins = new(); + this.WhenActivated(d => + { + _isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode) + .Select(tuple => tuple.Item1 || tuple.Item2) + .ToProperty(this, model => model.IsStaticNode) + .DisposeWith(d); + + Node.WhenAnyValue(n => n.Pins).Subscribe(pins => nodePins.Edit(source => + { + source.Clear(); + source.AddRange(pins); + })).DisposeWith(d); + }); + + DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); + + nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel).Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); + nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel).Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); + InputPinViewModels = inputPins; + OutputPinViewModels = outputPins; } + public bool IsStaticNode => _isStaticNode?.Value ?? true; + public INode Node { get; } + public ReadOnlyObservableCollection InputPinViewModels { get; } + public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } + public ReadOnlyObservableCollection OutputPinViewModels { get; } + public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; } - public Point Position + public ICustomNodeViewModel? CustomNodeViewModel { - get => _position; - set => RaiseAndSetIfChanged(ref _position, value); + get => _customNodeViewModel; + set => RaiseAndSetIfChanged(ref _customNodeViewModel, value); + } + + public ReactiveCommand? DeleteNode + { + get => _deleteNode; + set => RaiseAndSetIfChanged(ref _deleteNode, value); + } + + private void ExecuteDeleteNode() + { + _nodeEditorService.ExecuteCommand(_nodeScript, new DeleteNode(_nodeScript, Node)); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml index ae1b6724d..523457358 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml @@ -2,7 +2,30 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinView"> - Welcome to Avalonia! - + xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" + mc:Ignorable="d" + d:DesignWidth="200" + x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinView" + x:DataType="pins:PinViewModel"> + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs index ed148b458..098c76d3c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.Core.Services; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -6,7 +7,7 @@ public class InputPinViewModel : PinViewModel { public InputPin InputPin { get; } - public InputPinViewModel(InputPin inputPin) : base(inputPin) + public InputPinViewModel(InputPin inputPin, INodeService nodeService) : base(inputPin, nodeService) { InputPin = inputPin; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml index 893ff48e2..cb8e3d832 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml @@ -2,7 +2,30 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinView"> - Welcome to Avalonia! - + xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" + mc:Ignorable="d" + d:DesignWidth="200" + x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinView" + x:DataType="pins:PinViewModel"> + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs index 0719fdcab..8f0e8e82b 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.Core.Services; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -6,7 +7,7 @@ public class OutputPinViewModel : PinViewModel { public OutputPin OutputPin { get; } - public OutputPinViewModel(OutputPin outputPin) : base(outputPin) + public OutputPinViewModel(OutputPin outputPin, INodeService nodeService) : base(outputPin, nodeService) { OutputPin = outputPin; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs index ec321632a..0b1ce6202 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -1,14 +1,22 @@ using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Shared; +using Avalonia.Media; namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinViewModel : ActivatableViewModelBase { - protected PinViewModel(IPin pin) + protected PinViewModel(IPin pin, INodeService nodeService) { Pin = pin; + + TypeColorRegistration registration = nodeService.GetTypeColorRegistration(Pin.Type); + PinColor = new Color(registration.Color.Alpha, registration.Color.Red, registration.Color.Green, registration.Color.Blue); + DarkenedPinColor = new Color(registration.DarkenedColor.Alpha, registration.DarkenedColor.Red, registration.DarkenedColor.Green, registration.DarkenedColor.Blue); } public IPin Pin { get; } + public Color PinColor { get; } + public Color DarkenedPinColor { get; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml new file mode 100644 index 000000000..afd62ecf2 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml index 1580a3f95..6f972839c 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopView.axaml @@ -12,49 +12,54 @@ x:Class="Artemis.UI.Screens.Workshop.WorkshopView" x:DataType="workshop:WorkshopViewModel"> - - Workshop!! :3 - - - Notification tests - - - - + + + + Nodes tests + + + + + + Notification tests + + + + - + - - - - - - - + + + + + + - + - - + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index cb7f09681..694ad3741 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -2,6 +2,8 @@ using System.Reactive; using System.Reactive.Linq; using Artemis.Core; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using Avalonia.Input; @@ -25,15 +27,19 @@ namespace Artemis.UI.Screens.Workshop new ColorGradientStop(new SKColor(0xFF00FCCC), 1f), }; - public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService) : base(hostScreen, "workshop") + public WorkshopViewModel(IScreen hostScreen, INotificationService notificationService, INodeVmFactory nodeVmFactory) : base(hostScreen, "workshop") { _notificationService = notificationService; _cursor = this.WhenAnyValue(vm => vm.SelectedCursor).Select(c => new Cursor(c)).ToProperty(this, vm => vm.Cursor); DisplayName = "Workshop"; ShowNotification = ReactiveCommand.Create(ExecuteShowNotification); + + VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(new NodeScript("Test script", "A test script")); } + public NodeScriptViewModel VisualEditorViewModel { get; } + public ReactiveCommand ShowNotification { get; set; } public StandardCursorType SelectedCursor diff --git a/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs b/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs index 4c45d8b28..838947dbc 100644 --- a/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/Interfaces/IRegistrationService.cs @@ -7,5 +7,6 @@ void RegisterBuiltInPropertyEditors(); void RegisterControllers(); void ApplyPreferredGraphicsContext(); + void RegisterBuiltInNodeTypes(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs index f6f8e6c53..24574e35b 100644 --- a/src/Avalonia/Artemis.UI/Services/RegistrationService.cs +++ b/src/Avalonia/Artemis.UI/Services/RegistrationService.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.DefaultTypes.PropertyInput; @@ -6,9 +9,11 @@ using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.PropertyInput; +using Artemis.VisualScripting.Nodes; using Avalonia; using DynamicData; using Ninject; +using SkiaSharp; namespace Artemis.UI.Services; @@ -17,13 +22,15 @@ public class RegistrationService : IRegistrationService private readonly IKernel _kernel; private readonly IInputService _inputService; private readonly IPropertyInputService _propertyInputService; + private readonly INodeService _nodeService; private bool _registeredBuiltInPropertyEditors; - public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, IEnumerable toolViewModels) + public RegistrationService(IKernel kernel, IInputService inputService, IPropertyInputService propertyInputService, IProfileEditorService profileEditorService, INodeService nodeService, IEnumerable toolViewModels) { _kernel = kernel; _inputService = inputService; _propertyInputService = propertyInputService; + _nodeService = nodeService; profileEditorService.Tools.AddRange(toolViewModels); CreateCursorResources(); @@ -75,4 +82,18 @@ public class RegistrationService : IRegistrationService public void ApplyPreferredGraphicsContext() { } + + public void RegisterBuiltInNodeTypes() + { + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(bool), new SKColor(0xFFCD3232)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(string), new SKColor(0xFFFFD700)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(int), new SKColor(0xFF32CD32)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(float), new SKColor(0xFFFF7C00)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFFAD3EED)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFED3E61)); + _nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(Enum), new SKColor(0xFF1E90FF)); + + foreach (Type nodeType in typeof(SumNumericsNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface)) + _nodeService.RegisterNodeType(Constants.CorePlugin, nodeType); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index 9a5fbda19..1e975be58 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -481,6 +481,11 @@ "resolved": "5.0.0", "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "5.0.9", + "contentHash": "grj0e6Me0EQsgaurV0fxP0xd8sz8eZVK+Jb816DPzNADHaqXaXJD3xZX9SFjyDl3ykAYvD0y77o5vRd9Hzsk9g==" + }, "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "5.0.0", @@ -599,6 +604,14 @@ "Ninject": "3.3.3" } }, + "NoStringEvaluating": { + "type": "Transitive", + "resolved": "2.2.2", + "contentHash": "hJHivPDA1Vxn0CCgOtHKZ3fmldxQuz7VL1J4lEaPTXCf+Vwcx1FDf05mGMh6olYMSxoKimGX8YK2sEoqeH3pnA==", + "dependencies": { + "Microsoft.Extensions.ObjectPool": "5.0.9" + } + }, "RGB.NET.Presets": { "type": "Transitive", "resolved": "1.0.0-prerelease7", @@ -1783,6 +1796,19 @@ "ReactiveUI.Validation": "2.2.1", "SkiaSharp": "2.88.0-preview.178" } + }, + "artemis.visualscripting": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Artemis.UI.Shared": "1.0.0", + "Avalonia": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", + "Ninject": "3.3.4", + "NoStringEvaluating": "2.2.2", + "ReactiveUI": "17.1.50", + "SkiaSharp": "2.88.0-preview.178" + } } } } diff --git a/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj new file mode 100644 index 000000000..be4170e96 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -0,0 +1,74 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + + + + + + + diff --git a/src/Avalonia/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs b/src/Avalonia/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs new file mode 100644 index 000000000..3bf75bd56 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Ninject/NoStringNinjectModule.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Microsoft.Extensions.ObjectPool; +using Ninject.Modules; +using NoStringEvaluating; +using NoStringEvaluating.Contract; +using NoStringEvaluating.Models.Values; +using NoStringEvaluating.Services.Cache; +using NoStringEvaluating.Services.Checking; +using NoStringEvaluating.Services.Parsing; +using NoStringEvaluating.Services.Parsing.NodeReaders; + +namespace Artemis.VisualScripting.Ninject +{ + public class NoStringNinjectModule : NinjectModule + { + public override void Load() + { + // Pooling + Bind>>() + .ToConstant(ObjectPool.Create>()) + .InSingletonScope(); + + Bind>>() + .ToConstant(ObjectPool.Create>()) + .InSingletonScope(); + + Bind>() + .ToConstant(ObjectPool.Create()) + .InSingletonScope(); + + // Parser + Bind().To().InSingletonScope(); + Bind().To().InSingletonScope(); + Bind().To().InSingletonScope(); + + // Checker + Bind().To().InSingletonScope(); + + // Evaluator + Bind().To().InSingletonScope(); + + // If needed + InjectUserDefinedFunctions(); + } + + private void InjectUserDefinedFunctions() + { + IFunctionReader functionReader = (IFunctionReader) Kernel!.GetService(typeof(IFunctionReader)); + NoStringFunctionsInitializer.InitializeFunctions(functionReader, typeof(NoStringNinjectModule)); + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/BoolOperations.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/BoolOperations.cs new file mode 100644 index 000000000..a900a4fb1 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/BoolOperations.cs @@ -0,0 +1,299 @@ +using System.Collections; +using Artemis.Core; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes; + +[Node("Greater than", "Checks if the first input is greater than the second.", "Operators", InputType = typeof(object), OutputType = typeof(bool))] +public class GreaterThanNode : Node +{ + #region Constructors + + public GreaterThanNode() + : base("Greater than", "Checks if the first input is greater than the second.") + { + Input1 = CreateInputPin(); + Input2 = CreateInputPin(); + Result = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + if (Input1.Value is Numeric numeric1 && Input2.Value is Numeric numeric2) + { + Result.Value = numeric1 > numeric2; + return; + } + + if (Input2.Value != null && Input1.Value != null && Input1.Value.IsNumber() && Input2.Value.IsNumber()) + { + Result.Value = Convert.ToSingle(Input1.Value) > Convert.ToSingle(Input2.Value); + return; + } + + try + { + Result.Value = Comparer.DefaultInvariant.Compare(Input1.Value, Input2.Value) == 1; + } + catch + { + Result.Value = false; + } + } + + #endregion + + #region Properties & Fields + + public InputPin Input1 { get; } + public InputPin Input2 { get; } + + public OutputPin Result { get; } + + #endregion +} + +[Node("Less than", "Checks if the first input is less than the second.", "Operators", InputType = typeof(object), OutputType = typeof(bool))] +public class LessThanNode : Node +{ + #region Constructors + + public LessThanNode() + : base("Less than", "Checks if the first input is less than the second.") + { + Input1 = CreateInputPin(); + Input2 = CreateInputPin(); + Result = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + if (Input1.Value is Numeric numeric1 && Input2.Value is Numeric numeric2) + { + Result.Value = numeric1 < numeric2; + return; + } + + if (Input2.Value != null && Input1.Value != null && Input1.Value.IsNumber() && Input2.Value.IsNumber()) + { + Result.Value = Convert.ToSingle(Input1.Value) < Convert.ToSingle(Input2.Value); + return; + } + + try + { + Result.Value = Comparer.DefaultInvariant.Compare(Input1.Value, Input2.Value) == -1; + } + catch + { + Result.Value = false; + } + } + + #endregion + + #region Properties & Fields + + public InputPin Input1 { get; } + public InputPin Input2 { get; } + + public OutputPin Result { get; } + + #endregion +} + +[Node("Equals", "Checks if the two inputs are equals.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))] +public class EqualsNode : Node +{ + #region Constructors + + public EqualsNode() + : base("Equals", "Checks if the two inputs are equals.") + { + Input1 = CreateInputPin(); + Input2 = CreateInputPin(); + Result = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + try + { + Result.Value = Equals(Input1.Value, Input2.Value); + } + catch + { + Result.Value = false; + } + } + + #endregion + + #region Properties & Fields + + public InputPin Input1 { get; } + public InputPin Input2 { get; } + + public OutputPin Result { get; } + + #endregion +} + +[Node("Negate", "Negates the boolean.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))] +public class NegateNode : Node +{ + #region Constructors + + public NegateNode() + : base("Negate", "Negates the boolean.") + { + Input = CreateInputPin(); + Output = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = !Input.Value; + } + + #endregion + + #region Properties & Fields + + public InputPin Input { get; } + public OutputPin Output { get; } + + #endregion +} + +[Node("And", "Checks if all inputs are true.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))] +public class AndNode : Node +{ + #region Constructors + + public AndNode() + : base("And", "Checks if all inputs are true.") + { + Input = CreateInputPinCollection(); + Result = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Result.Value = Input.Values.All(v => v); + } + + #endregion + + #region Properties & Fields + + public InputPinCollection Input { get; set; } + public OutputPin Result { get; } + + #endregion +} + +[Node("Or", "Checks if any inputs are true.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))] +public class OrNode : Node +{ + #region Constructors + + public OrNode() + : base("Or", "Checks if any inputs are true.") + { + Input = CreateInputPinCollection(); + Result = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Result.Value = Input.Values.Any(v => v); + } + + #endregion + + #region Properties & Fields + + public InputPinCollection Input { get; set; } + public OutputPin Result { get; } + + #endregion +} + +[Node("Exclusive Or", "Checks if one of the inputs is true.", "Operators", InputType = typeof(bool), OutputType = typeof(bool))] +public class XorNode : Node +{ + #region Constructors + + public XorNode() + : base("Exclusive Or", "Checks if one of the inputs is true.") + { + Input = CreateInputPinCollection(); + Result = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Result.Value = Input.Values.Count(v => v) == 1; + } + + #endregion + + #region Properties & Fields + + public InputPinCollection Input { get; set; } + public OutputPin Result { get; } + + #endregion +} + +[Node("Enum Equals", "Determines the equality between an input and a selected enum value", "Operators", InputType = typeof(Enum), OutputType = typeof(bool))] +public class EnumEqualsNode : Node +{ + public EnumEqualsNode() : base("Enum Equals", "Determines the equality between an input and a selected enum value") + { + InputPin = CreateInputPin(); + OutputPin = CreateOutputPin(); + } + + public InputPin InputPin { get; } + public OutputPin OutputPin { get; } + + #region Overrides of Node + + /// + public override void Evaluate() + { + OutputPin.Value = InputPin.Value != null && InputPin.Value.Equals(Storage); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/BrightenSKColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/BrightenSKColorNode.cs new file mode 100644 index 000000000..384d09d88 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/BrightenSKColorNode.cs @@ -0,0 +1,26 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Brighten Color", "Brightens a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class BrightenSKColorNode : Node +{ + public BrightenSKColorNode() : base("Brighten Color", "Brightens a color by a specified amount in percent") + { + Input = CreateInputPin("Color"); + Percentage = CreateInputPin("%"); + Output = CreateOutputPin(); + } + + public InputPin Input { get; } + public InputPin Percentage { get; } + public OutputPin Output { get; set; } + + public override void Evaluate() + { + Input.Value.ToHsl(out float h, out float s, out float l); + l += l * (Percentage.Value / 100f); + Output.Value = SKColor.FromHsl(h, s, l); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs new file mode 100644 index 000000000..a71435a36 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs @@ -0,0 +1,10 @@ +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.Color.CustomViewModels; + +public class StaticSKColorValueNodeCustomViewModel : CustomNodeViewModel +{ + public StaticSKColorValueNodeCustomViewModel(StaticSKColorValueNode node) : base(node) + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViews/StaticSKColorValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViews/StaticSKColorValueNodeCustomView.xaml new file mode 100644 index 000000000..eb02e7984 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViews/StaticSKColorValueNodeCustomView.xaml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/DarkenSKColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/DarkenSKColorNode.cs new file mode 100644 index 000000000..8ba31d1d6 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/DarkenSKColorNode.cs @@ -0,0 +1,26 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Darken Color", "Darkens a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class DarkenSKColorNode : Node +{ + public DarkenSKColorNode() : base("Darken Color", "Darkens a color by a specified amount in percent") + { + Input = CreateInputPin("Color"); + Percentage = CreateInputPin("%"); + Output = CreateOutputPin(); + } + + public InputPin Input { get; } + public InputPin Percentage { get; } + public OutputPin Output { get; set; } + + public override void Evaluate() + { + Input.Value.ToHsl(out float h, out float s, out float l); + l -= l * (Percentage.Value / 100f); + Output.Value = SKColor.FromHsl(h, s, l); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/DesaturateSKColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/DesaturateSKColorNode.cs new file mode 100644 index 000000000..818b77bb4 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/DesaturateSKColorNode.cs @@ -0,0 +1,26 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Desaturate Color", "Desaturates a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class DesaturateSKColorNode : Node +{ + public DesaturateSKColorNode() : base("Desaturate Color", "Desaturates a color by a specified amount in percent") + { + Input = CreateInputPin("Color"); + Percentage = CreateInputPin("%"); + Output = CreateOutputPin(); + } + + public InputPin Input { get; } + public InputPin Percentage { get; } + public OutputPin Output { get; set; } + + public override void Evaluate() + { + Input.Value.ToHsl(out float h, out float s, out float l); + s -= s * (Percentage.Value / 100f); + Output.Value = SKColor.FromHsl(h, Math.Clamp(s, 0f, 100f), l); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/HslSKColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/HslSKColorNode.cs new file mode 100644 index 000000000..558041355 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/HslSKColorNode.cs @@ -0,0 +1,31 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("HSL Color", "Creates a color from hue, saturation and lightness values", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))] +public class HslSKColorNode : Node +{ + public HslSKColorNode() : base("HSL Color", "Creates a color from hue, saturation and lightness values") + { + H = CreateInputPin("H"); + S = CreateInputPin("S"); + L = CreateInputPin("L"); + Output = CreateOutputPin(); + } + + public InputPin H { get; set; } + public InputPin S { get; set; } + public InputPin L { get; set; } + public OutputPin Output { get; } + + #region Overrides of Node + + /// + public override void Evaluate() + { + Output.Value = SKColor.FromHsl(H.Value, S.Value, L.Value); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/InvertSKColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/InvertSKColorNode.cs new file mode 100644 index 000000000..5abf00994 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/InvertSKColorNode.cs @@ -0,0 +1,27 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Invert Color", "Inverts a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class InvertSKColorNode : Node +{ + public InvertSKColorNode() : base("Invert Color", "Inverts a color") + { + Input = CreateInputPin(); + Output = CreateOutputPin(); + } + + public InputPin Input { get; } + public OutputPin Output { get; set; } + + public override void Evaluate() + { + Output.Value = new SKColor( + (byte) (255 - Input.Value.Red), + (byte) (255 - Input.Value.Green), + (byte) (255 - Input.Value.Blue), + Input.Value.Alpha + ); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/RotateHueSkColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/RotateHueSkColorNode.cs new file mode 100644 index 000000000..22d16ef53 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/RotateHueSkColorNode.cs @@ -0,0 +1,26 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Rotate Color Hue", "Rotates the hue of a color by a specified amount in degrees", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class RotateHueSKColorNode : Node +{ + public RotateHueSKColorNode() : base("Rotate Color Hue", "Rotates the hue of a color by a specified amount in degrees") + { + Input = CreateInputPin("Color"); + Amount = CreateInputPin("Amount"); + Output = CreateOutputPin(); + } + + public InputPin Input { get; } + public InputPin Amount { get; } + public OutputPin Output { get; set; } + + public override void Evaluate() + { + Input.Value.ToHsl(out float h, out float s, out float l); + h += Amount.Value; + Output.Value = SKColor.FromHsl(h % 360, s, l); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/SaturateSkColorNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/SaturateSkColorNode.cs new file mode 100644 index 000000000..252db28dc --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/SaturateSkColorNode.cs @@ -0,0 +1,26 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Saturate Color", "Saturates a color by a specified amount in percent", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class SaturateSKColorNode : Node +{ + public SaturateSKColorNode() : base("Saturate Color", "Saturates a color by a specified amount in percent") + { + Input = CreateInputPin("Color"); + Percentage = CreateInputPin("%"); + Output = CreateOutputPin(); + } + + public InputPin Input { get; } + public InputPin Percentage { get; } + public OutputPin Output { get; set; } + + public override void Evaluate() + { + Input.Value.ToHsl(out float h, out float s, out float l); + s += s * (Percentage.Value / 100f); + Output.Value = SKColor.FromHsl(h, Math.Clamp(s, 0f, 100f), l); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/StaticSKColorValueNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/StaticSKColorValueNode.cs new file mode 100644 index 000000000..b6f0512f5 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/StaticSKColorValueNode.cs @@ -0,0 +1,34 @@ +using Artemis.Core; +using Artemis.VisualScripting.Nodes.Color.CustomViewModels; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Color-Value", "Outputs a configurable color value.", "Static", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class StaticSKColorValueNode : Node +{ + #region Constructors + + public StaticSKColorValueNode() + : base("Color", "Outputs a configurable color value.") + { + Output = CreateOutputPin(); + } + + #endregion + + #region Properties & Fields + + public OutputPin Output { get; } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = Storage; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/SumSKColorsNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/SumSKColorsNode.cs new file mode 100644 index 000000000..a67a5238f --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/SumSKColorsNode.cs @@ -0,0 +1,45 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Color; + +[Node("Sum (Color)", "Sums the connected color values.", "Color", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class SumSKColorsNode : Node +{ + #region Constructors + + public SumSKColorsNode() + : base("Sum", "Sums the connected color values.") + { + Values = CreateInputPinCollection("Values", 2); + Sum = CreateOutputPin("Sum"); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + SKColor result = SKColor.Empty; + + bool first = true; + foreach (SKColor current in Values.Values) + { + result = first ? current : result.Sum(current); + first = false; + } + + Sum.Value = result; + } + + #endregion + + #region Properties & Fields + + public InputPinCollection Values { get; } + + public OutputPin Sum { get; } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/ConvertNodes.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/ConvertNodes.cs new file mode 100644 index 000000000..a3358fd31 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/ConvertNodes.cs @@ -0,0 +1,80 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes; + +[Node("To String", "Converts the input to a string.", "Conversion", InputType = typeof(object), OutputType = typeof(string))] +public class ConvertToStringNode : Node +{ + #region Constructors + + public ConvertToStringNode() + : base("To String", "Converts the input to a string.") + { + Input = CreateInputPin(); + String = CreateOutputPin(); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + String.Value = Input.Value?.ToString(); + } + + #endregion + + #region Properties & Fields + + public InputPin Input { get; } + + public OutputPin String { get; } + + #endregion +} + +[Node("To Numeric", "Converts the input to a numeric.", "Conversion", InputType = typeof(object), OutputType = typeof(Numeric))] +public class ConvertToNumericNode : Node +{ + #region Constructors + + public ConvertToNumericNode() + : base("To Numeric", "Converts the input to a numeric.") + { + Input = CreateInputPin(); + Output = CreateOutputPin(); + } + + #endregion + + #region Properties & Fields + + public InputPin Input { get; } + + public OutputPin Output { get; } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = Input.Value switch + { + int input => new Numeric(input), + double input => new Numeric(input), + float input => new Numeric(input), + byte input => new Numeric(input), + _ => TryParse(Input.Value) + }; + } + + private Numeric TryParse(object input) + { + Numeric.TryParse(input?.ToString(), out Numeric value); + return value; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs new file mode 100644 index 000000000..54d8bbc7e --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs @@ -0,0 +1,49 @@ +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.UI.Shared.VisualScripting; +using DynamicData; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels; + +public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel +{ + private readonly EnumEqualsNode _node; + + public EnumEqualsNodeCustomViewModel(EnumEqualsNode node) : base(node) + { + _node = node; + } + + public ObservableCollection<(Enum, string)> EnumValues { get; } = new(); + + // public override void OnActivate() + // { + // _node.InputPin.PinConnected += InputPinOnPinConnected; + // _node.InputPin.PinDisconnected += InputPinOnPinDisconnected; + // + // if (_node.InputPin.Value != null && _node.InputPin.Value.GetType().IsEnum) + // EnumValues.AddRange(EnumUtilities.GetAllValuesAndDescriptions(_node.InputPin.Value.GetType())); + // base.OnActivate(); + // } + // + // public override void OnDeactivate() + // { + // _node.InputPin.PinConnected -= InputPinOnPinConnected; + // _node.InputPin.PinDisconnected -= InputPinOnPinDisconnected; + // + // base.OnDeactivate(); + // } + + private void InputPinOnPinDisconnected(object sender, SingleValueEventArgs e) + { + EnumValues.Clear(); + } + + private void InputPinOnPinConnected(object sender, SingleValueEventArgs e) + { + EnumValues.Clear(); + if (_node.InputPin.Value != null && _node.InputPin.Value.GetType().IsEnum) + EnumValues.AddRange(EnumUtilities.GetAllValuesAndDescriptions(_node.InputPin.Value.GetType())); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs new file mode 100644 index 000000000..f11a6ab08 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs @@ -0,0 +1,73 @@ +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels; + +public class LayerPropertyNodeCustomViewModel : CustomNodeViewModel +{ + private readonly LayerPropertyNode _node; + private ILayerProperty _selectedLayerProperty; + + private RenderProfileElement _selectedProfileElement; + + public LayerPropertyNodeCustomViewModel(LayerPropertyNode node) : base(node) + { + _node = node; + } + + public ObservableCollection ProfileElements { get; } = new(); + + // public RenderProfileElement SelectedProfileElement + // { + // get => _selectedProfileElement; + // set + // { + // if (!SetAndNotify(ref _selectedProfileElement, value)) return; + // _node.ChangeProfileElement(_selectedProfileElement); + // GetLayerProperties(); + // } + // } + + public ObservableCollection LayerProperties { get; } = new(); + + // public ILayerProperty SelectedLayerProperty + // { + // get => _selectedLayerProperty; + // set + // { + // if (!SetAndNotify(ref _selectedLayerProperty, value)) return; + // _node.ChangeLayerProperty(_selectedLayerProperty); + // } + // } + + // private void GetProfileElements() + // { + // ProfileElements.Clear(); + // if (_node.Script.Context is not Profile profile) + // return; + // + // List elements = new(profile.GetAllRenderElements()); + // + // ProfileElements.AddRange(elements.OrderBy(e => e.Order)); + // _selectedProfileElement = _node.ProfileElement; + // NotifyOfPropertyChange(nameof(SelectedProfileElement)); + // } + // + // private void GetLayerProperties() + // { + // LayerProperties.Clear(); + // if (_node.ProfileElement == null) + // return; + // + // LayerProperties.AddRange(_node.ProfileElement.GetAllLayerProperties().Where(l => !l.IsHidden && l.DataBindingsSupported)); + // _selectedLayerProperty = _node.LayerProperty; + // NotifyOfPropertyChange(nameof(SelectedLayerProperty)); + // } + // + // public override void OnActivate() + // { + // GetProfileElements(); + // GetLayerProperties(); + // } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs new file mode 100644 index 000000000..271e3d4dc --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs @@ -0,0 +1,18 @@ +using Artemis.Core; +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.CustomViewModels; + +public class StaticNumericValueNodeCustomViewModel : CustomNodeViewModel +{ + public StaticNumericValueNodeCustomViewModel(INode node) : base(node) + { + } +} + +public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel +{ + public StaticStringValueNodeCustomViewModel(INode node) : base(node) + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml new file mode 100644 index 000000000..da9a0378f --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml new file mode 100644 index 000000000..2b526f152 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml new file mode 100644 index 000000000..6c43dfcbf --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml new file mode 100644 index 000000000..54335410e --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs new file mode 100644 index 000000000..daeb894dc --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs @@ -0,0 +1,72 @@ +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; + +public class DataModelEventNodeCustomViewModel : CustomNodeViewModel +{ + private readonly DataModelEventNode _node; + private ObservableCollection _modules; + + public DataModelEventNodeCustomViewModel(DataModelEventNode node, ISettingsService settingsService) : base(node) + { + _node = node; + + ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); + ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + } + + public PluginSetting ShowFullPaths { get; } + public PluginSetting ShowDataModelValues { get; } + public ObservableCollection FilterTypes { get; } = new() {typeof(IDataModelEvent)}; + + // public ObservableCollection Modules + // { + // get => _modules; + // set => SetAndNotify(ref _modules, value); + // } + + public DataModelPath DataModelPath + { + get => _node.DataModelPath; + set + { + if (ReferenceEquals(_node.DataModelPath, value)) + return; + + _node.DataModelPath?.Dispose(); + _node.DataModelPath = value; + _node.DataModelPath.Save(); + + _node.Storage = _node.DataModelPath.Entity; + } + } + + // public override void OnActivate() + // { + // if (Modules != null) + // return; + // + // Modules = new ObservableCollection(); + // if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + // Modules.Add(scriptProfile.Configuration.Module); + // else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + // Modules.Add(profileConfiguration.Module); + // + // _node.PropertyChanged += NodeOnPropertyChanged; + // } + // + // public override void OnDeactivate() + // { + // _node.PropertyChanged -= NodeOnPropertyChanged; + // } + // + // private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) + // { + // if (e.PropertyName == nameof(DataModelNode.DataModelPath)) + // OnPropertyChanged(nameof(DataModelPath)); + // } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs new file mode 100644 index 000000000..f62bc7533 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs @@ -0,0 +1,72 @@ +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.Core.Services; +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; + +public class DataModelNodeCustomViewModel : CustomNodeViewModel +{ + private readonly DataModelNode _node; + private ObservableCollection _modules; + + public DataModelNodeCustomViewModel(DataModelNode node, ISettingsService settingsService) : base(node) + { + _node = node; + + ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); + ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + } + + public PluginSetting ShowFullPaths { get; } + public PluginSetting ShowDataModelValues { get; } + + // public ObservableCollection Modules + // { + // get => _modules; + // set => SetAndNotify(ref _modules, value); + // } + + public DataModelPath DataModelPath + { + get => _node.DataModelPath; + set + { + if (ReferenceEquals(_node.DataModelPath, value)) + return; + + _node.DataModelPath?.Dispose(); + _node.DataModelPath = value; + _node.DataModelPath.Save(); + + _node.Storage = _node.DataModelPath.Entity; + _node.UpdateOutputPin(false); + } + } + + // public override void OnActivate() + // { + // if (Modules != null) + // return; + // + // Modules = new ObservableCollection(); + // if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + // Modules.Add(scriptProfile.Configuration.Module); + // else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + // Modules.Add(profileConfiguration.Module); + // + // _node.PropertyChanged += NodeOnPropertyChanged; + // } + // + // public override void OnDeactivate() + // { + // _node.PropertyChanged -= NodeOnPropertyChanged; + // } + // + // private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) + // { + // if (e.PropertyName == nameof(DataModelNode.DataModelPath)) + // OnPropertyChanged(nameof(DataModelPath)); + // } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.xaml new file mode 100644 index 000000000..924179fe4 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.xaml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.xaml new file mode 100644 index 000000000..ada3716fa --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.xaml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs new file mode 100644 index 000000000..c720da2ae --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs @@ -0,0 +1,187 @@ +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.Storage.Entities.Profile; +using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes.DataModel; + +[Node("Data Model-Event", "Responds to a data model event trigger", "Data Model", OutputType = typeof(bool))] +public class DataModelEventNode : Node, IDisposable +{ + private int _currentIndex; + private Type _currentType; + private DataModelPath _dataModelPath; + private DateTime _lastTrigger; + private bool _updating; + + public DataModelEventNode() : base("Data Model-Event", "Responds to a data model event trigger") + { + _currentType = typeof(object); + CreateCycleValues(typeof(object), 1); + Output = CreateOutputPin(typeof(object)); + } + + public InputPinCollection CycleValues { get; set; } + public OutputPin Output { get; set; } + public INodeScript Script { get; set; } + + public DataModelPath DataModelPath + { + get => _dataModelPath; + set => SetAndNotify(ref _dataModelPath, value); + } + + public override void Initialize(INodeScript script) + { + Script = script; + + if (Storage == null) + return; + + DataModelPath = new DataModelPath(Storage); + } + + public override void Evaluate() + { + object outputValue = null; + if (DataModelPath?.GetValue() is IDataModelEvent dataModelEvent) + { + if (dataModelEvent.LastTrigger > _lastTrigger) + { + _lastTrigger = dataModelEvent.LastTrigger; + _currentIndex++; + + if (_currentIndex >= CycleValues.Count()) + _currentIndex = 0; + } + + outputValue = CycleValues.ElementAt(_currentIndex).PinValue; + } + + if (Output.Type.IsInstanceOfType(outputValue)) + Output.Value = outputValue; + else if (Output.Type.IsValueType) + Output.Value = Output.Type.GetDefault()!; + } + + private void CreateCycleValues(Type type, int initialCount) + { + if (CycleValues != null) + { + CycleValues.PinAdded -= CycleValuesOnPinAdded; + CycleValues.PinRemoved -= CycleValuesOnPinRemoved; + foreach (IPin pin in CycleValues) + { + pin.PinConnected -= OnPinConnected; + pin.PinDisconnected -= OnPinDisconnected; + } + + RemovePinCollection(CycleValues); + } + + CycleValues = CreateInputPinCollection(type, "", initialCount); + CycleValues.PinAdded += CycleValuesOnPinAdded; + CycleValues.PinRemoved += CycleValuesOnPinRemoved; + foreach (IPin pin in CycleValues) + { + pin.PinConnected += OnPinConnected; + pin.PinDisconnected += OnPinDisconnected; + } + } + + private void CycleValuesOnPinAdded(object sender, SingleValueEventArgs e) + { + e.Value.PinConnected += OnPinConnected; + e.Value.PinDisconnected += OnPinDisconnected; + } + + private void CycleValuesOnPinRemoved(object sender, SingleValueEventArgs e) + { + e.Value.PinConnected -= OnPinConnected; + e.Value.PinDisconnected -= OnPinDisconnected; + } + + private void OnPinDisconnected(object sender, SingleValueEventArgs e) + { + ProcessPinDisconnected(); + } + + private void OnPinConnected(object sender, SingleValueEventArgs e) + { + IPin source = e.Value; + IPin target = (IPin) sender; + ProcessPinConnected(source, target); + } + + private void ProcessPinConnected(IPin source, IPin target) + { + if (_updating) + return; + + try + { + _updating = true; + + // No need to change anything if the types haven't changed + if (_currentType == source.Type) + return; + + int reconnectIndex = CycleValues.ToList().IndexOf(target); + ChangeCurrentType(source.Type); + + if (reconnectIndex != -1) + { + CycleValues.ToList()[reconnectIndex].ConnectTo(source); + source.ConnectTo(CycleValues.ToList()[reconnectIndex]); + } + } + finally + { + _updating = false; + } + } + + private void ChangeCurrentType(Type type) + { + CreateCycleValues(type, CycleValues.Count()); + + List oldOutputConnections = Output.ConnectedTo.ToList(); + RemovePin(Output); + Output = CreateOutputPin(type); + foreach (IPin oldOutputConnection in oldOutputConnections.Where(o => o.Type.IsAssignableFrom(Output.Type))) + { + oldOutputConnection.DisconnectAll(); + oldOutputConnection.ConnectTo(Output); + Output.ConnectTo(oldOutputConnection); + } + + _currentType = type; + } + + private void ProcessPinDisconnected() + { + if (_updating) + return; + try + { + // If there's still a connected pin, stick to the current type + if (CycleValues.Any(v => v.ConnectedTo.Any())) + return; + + ChangeCurrentType(typeof(object)); + } + finally + { + _updating = false; + } + } + + private void UpdatePinsType(IPin source) + { + } + + /// + public void Dispose() + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs new file mode 100644 index 000000000..0c0239709 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs @@ -0,0 +1,95 @@ +using Artemis.Core; +using Artemis.Storage.Entities.Profile; +using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; +using Avalonia.Threading; + +namespace Artemis.VisualScripting.Nodes.DataModel; + +[Node("Data Model-Value", "Outputs a selectable data model value.", "Data Model")] +public class DataModelNode : Node, IDisposable +{ + private DataModelPath _dataModelPath; + + public DataModelNode() : base("Data Model", "Outputs a selectable data model value") + { + } + + public INodeScript Script { get; private set; } + public OutputPin Output { get; private set; } + + public DataModelPath DataModelPath + { + get => _dataModelPath; + set => SetAndNotify(ref _dataModelPath, value); + } + + public override void Initialize(INodeScript script) + { + Script = script; + + if (Storage == null) + return; + + DataModelPath = new DataModelPath(Storage); + DataModelPath.PathValidated += DataModelPathOnPathValidated; + + UpdateOutputPin(false); + } + + public override void Evaluate() + { + if (DataModelPath.IsValid) + { + if (Output == null) + UpdateOutputPin(false); + + object pathValue = DataModelPath.GetValue(); + + if (pathValue == null) + { + if (!Output.Type.IsValueType) + Output.Value = null; + } + else + { + if (Output.Type == typeof(Numeric)) + Output.Value = new Numeric(pathValue); + else + Output.Value = pathValue; + } + } + } + + public void UpdateOutputPin(bool loadConnections) + { + Type type = DataModelPath?.GetPropertyType(); + if (Numeric.IsTypeCompatible(type)) + type = typeof(Numeric); + + if (Output != null && Output.Type == type) + return; + + if (Output != null) + { + RemovePin(Output); + Output = null; + } + + if (type != null) + Output = CreateOutputPin(type); + + if (loadConnections && Script is NodeScript nodeScript) + nodeScript.LoadConnections(); + } + + private void DataModelPathOnPathValidated(object sender, EventArgs e) + { + Dispatcher.UIThread.InvokeAsync(() => UpdateOutputPin(true)); + } + + /// + public void Dispose() + { + DataModelPath?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs new file mode 100644 index 000000000..ae4a1de1f --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs @@ -0,0 +1,54 @@ +using System.Collections.ObjectModel; +using Artemis.Core; +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.Easing.CustomViewModels; + +public class EasingTypeNodeCustomViewModel : CustomNodeViewModel +{ + private readonly EasingTypeNode _node; + private NodeEasingViewModel _selectedEasingViewModel; + + public EasingTypeNodeCustomViewModel(EasingTypeNode node) : base(node) + { + _node = node; + EasingViewModels = new ObservableCollection(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(e => new NodeEasingViewModel(e))); + } + + public ObservableCollection EasingViewModels { get; } + + public NodeEasingViewModel SelectedEasingViewModel + { + get => _selectedEasingViewModel; + set + { + _selectedEasingViewModel = value; + _node.Storage = _selectedEasingViewModel.EasingFunction; + } + } + + // public override void OnActivate() + // { + // _node.PropertyChanged += NodeOnPropertyChanged; + // SelectedEasingViewModel = GetNodeEasingViewModel(); + // } + // + // public override void OnDeactivate() + // { + // _node.PropertyChanged -= NodeOnPropertyChanged; + // } + // + // private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) + // { + // if (e.PropertyName == nameof(_node.Storage)) + // { + // _selectedEasingViewModel = GetNodeEasingViewModel(); + // NotifyOfPropertyChange(nameof(SelectedEasingViewModel)); + // } + // } + + private NodeEasingViewModel GetNodeEasingViewModel() + { + return EasingViewModels.FirstOrDefault(vm => vm.EasingFunction == _node.Storage); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/NodeEasingViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/NodeEasingViewModel.cs new file mode 100644 index 000000000..9fb73fd42 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/NodeEasingViewModel.cs @@ -0,0 +1,27 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Avalonia; +using Humanizer; + +namespace Artemis.VisualScripting.Nodes.Easing.CustomViewModels; + +public class NodeEasingViewModel : ViewModelBase +{ + public NodeEasingViewModel(Easings.Functions easingFunction) + { + EasingFunction = easingFunction; + Description = easingFunction.Humanize(); + + EasingPoints = new List(); + for (int i = 1; i <= 10; i++) + { + int x = i; + double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10; + EasingPoints.Add(new Point(x, y)); + } + } + + public Easings.Functions EasingFunction { get; } + public List EasingPoints { get; } + public string Description { get; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml new file mode 100644 index 000000000..1930207eb --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/EasingTypeNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/EasingTypeNode.cs new file mode 100644 index 000000000..a4e2717a6 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/EasingTypeNode.cs @@ -0,0 +1,20 @@ +using Artemis.Core; +using Artemis.VisualScripting.Nodes.Easing.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes.Easing; + +[Node("Easing Type", "Outputs a selectable easing type.", "Easing", OutputType = typeof(Easings.Functions))] +public class EasingTypeNode : Node +{ + public EasingTypeNode() : base("Easing Type", "Outputs a selectable easing type.") + { + Output = CreateOutputPin(); + } + + public OutputPin Output { get; } + + public override void Evaluate() + { + Output.Value = Storage; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/NumericEasingNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/NumericEasingNode.cs new file mode 100644 index 000000000..9fe327f8a --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/NumericEasingNode.cs @@ -0,0 +1,66 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes.Easing; + +[Node("Numeric Easing", "Outputs an eased numeric value", "Easing", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class NumericEasingNode : Node +{ + private float _currentValue; + private DateTime _lastEvaluate = DateTime.MinValue; + private float _progress; + private float _sourceValue; + private float _targetValue; + + public NumericEasingNode() : base("Numeric Easing", "Outputs an eased numeric value") + { + Input = CreateInputPin(); + EasingTime = CreateInputPin("delay"); + EasingFunction = CreateInputPin("function"); + + Output = CreateOutputPin(); + } + + public InputPin Input { get; set; } + public InputPin EasingTime { get; set; } + public InputPin EasingFunction { get; set; } + + public OutputPin Output { get; set; } + + public override void Evaluate() + { + DateTime now = DateTime.Now; + + // If the value changed reset progress + if (Math.Abs(_targetValue - Input.Value) > 0.001f) + { + _sourceValue = _currentValue; + _targetValue = Input.Value; + _progress = 0f; + } + + // Update until finished + if (_progress < 1f) + { + Update(); + Output.Value = new Numeric(_currentValue); + } + // Stop updating past 1 and use the target value + else + { + Output.Value = new Numeric(_targetValue); + } + + _lastEvaluate = now; + } + + private void Update() + { + TimeSpan delta = DateTime.Now - _lastEvaluate; + + // In case of odd delta's, keep progress between 0f and 1f + _progress = Math.Clamp(_progress + (float) delta.TotalMilliseconds / EasingTime.Value, 0f, 1f); + + double eased = _sourceValue + (_targetValue - _sourceValue) * Easings.Interpolate(_progress, EasingFunction.Value); + _currentValue = (float) eased; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/SKColorEasingNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/SKColorEasingNode.cs new file mode 100644 index 000000000..9c2d50062 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/SKColorEasingNode.cs @@ -0,0 +1,65 @@ +using Artemis.Core; +using SkiaSharp; + +namespace Artemis.VisualScripting.Nodes.Easing; + +[Node("Color Easing", "Outputs an eased color value", "Easing", InputType = typeof(SKColor), OutputType = typeof(SKColor))] +public class SKColorEasingNode : Node +{ + private SKColor _currentValue; + private DateTime _lastEvaluate = DateTime.MinValue; + private float _progress; + private SKColor _sourceValue; + private SKColor _targetValue; + + public SKColorEasingNode() : base("Color Easing", "Outputs an eased color value") + { + Input = CreateInputPin(); + EasingTime = CreateInputPin("delay"); + EasingFunction = CreateInputPin("function"); + + Output = CreateOutputPin(); + } + + public InputPin Input { get; set; } + public InputPin EasingTime { get; set; } + public InputPin EasingFunction { get; set; } + + public OutputPin Output { get; set; } + + public override void Evaluate() + { + DateTime now = DateTime.Now; + + // If the value changed reset progress + if (_targetValue != Input.Value) + { + _sourceValue = _currentValue; + _targetValue = Input.Value; + _progress = 0f; + } + + // Update until finished + if (_progress < 1f) + { + Update(); + Output.Value = _currentValue; + } + // Stop updating past 1 and use the target value + else + { + Output.Value = _targetValue; + } + + _lastEvaluate = now; + } + + private void Update() + { + TimeSpan delta = DateTime.Now - _lastEvaluate; + + // In case of odd delta's, keep progress between 0f and 1f + _progress = Math.Clamp(_progress + (float) delta.TotalMilliseconds / EasingTime.Value, 0f, 1f); + _currentValue = _sourceValue.Interpolate(_targetValue, (float) Easings.Interpolate(_progress, EasingFunction.Value)); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs new file mode 100644 index 000000000..c1511c841 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/LayerPropertyNode.cs @@ -0,0 +1,124 @@ +using Artemis.Core; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes; + +[Node("Layer/Folder Property", "Outputs the property of a selected layer or folder", "External")] +public class LayerPropertyNode : Node +{ + private readonly object _layerPropertyLock = new(); + + public INodeScript Script { get; private set; } + public RenderProfileElement ProfileElement { get; private set; } + public ILayerProperty LayerProperty { get; private set; } + + public override void Evaluate() + { + lock (_layerPropertyLock) + { + // In this case remove the pins so no further evaluations occur + if (LayerProperty == null) + { + CreatePins(); + return; + } + + List list = LayerProperty.BaseDataBinding.Properties.ToList(); + int index = 0; + foreach (IPin pin in Pins) + { + OutputPin outputPin = (OutputPin) pin; + IDataBindingProperty dataBindingProperty = list[index]; + index++; + + // TODO: Is this really non-nullable? + outputPin.Value = dataBindingProperty.GetValue(); + } + } + } + + public override void Initialize(INodeScript script) + { + Script = script; + + if (script.Context is Profile profile) + profile.ChildRemoved += ProfileOnChildRemoved; + + LoadLayerProperty(); + } + + public void LoadLayerProperty() + { + lock (_layerPropertyLock) + { + if (Script.Context is not Profile profile || Storage == null) + return; + + RenderProfileElement element = profile.GetAllRenderElements().FirstOrDefault(l => l.EntityId == Storage.ElementId); + + ProfileElement = element; + LayerProperty = element?.GetAllLayerProperties().FirstOrDefault(p => p.Path == Storage.PropertyPath); + CreatePins(); + } + } + + public void ChangeProfileElement(RenderProfileElement profileElement) + { + lock (_layerPropertyLock) + { + ProfileElement = profileElement; + LayerProperty = null; + + Storage = new LayerPropertyNodeEntity + { + ElementId = ProfileElement?.EntityId ?? Guid.Empty, + PropertyPath = null + }; + + CreatePins(); + } + } + + public void ChangeLayerProperty(ILayerProperty layerProperty) + { + lock (_layerPropertyLock) + { + LayerProperty = layerProperty; + + Storage = new LayerPropertyNodeEntity + { + ElementId = ProfileElement?.EntityId ?? Guid.Empty, + PropertyPath = LayerProperty?.Path + }; + + CreatePins(); + } + } + + private void CreatePins() + { + while (Pins.Any()) + RemovePin((Pin) Pins.First()); + + if (LayerProperty == null) + return; + + foreach (IDataBindingProperty dataBindingRegistration in LayerProperty.BaseDataBinding.Properties) + CreateOutputPin(dataBindingRegistration.ValueType, dataBindingRegistration.DisplayName); + } + + private void ProfileOnChildRemoved(object sender, EventArgs e) + { + if (Script.Context is not Profile profile) + return; + + if (!profile.GetAllRenderElements().Contains(ProfileElement)) + ChangeProfileElement(null); + } +} + +public class LayerPropertyNodeEntity +{ + public Guid ElementId { get; set; } + public string PropertyPath { get; set; } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs new file mode 100644 index 000000000..4bbe43396 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs @@ -0,0 +1,11 @@ +using Artemis.Core; +using Artemis.UI.Shared.VisualScripting; + +namespace Artemis.VisualScripting.Nodes.Maths.CustomViewModels; + +public class MathExpressionNodeCustomViewModel : CustomNodeViewModel +{ + public MathExpressionNodeCustomViewModel(INode node) : base(node) + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViews/MathExpressionNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViews/MathExpressionNodeCustomView.xaml new file mode 100644 index 000000000..e39eb33ca --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViews/MathExpressionNodeCustomView.xaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/ExpressionNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/ExpressionNode.cs new file mode 100644 index 000000000..4c41be75c --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/ExpressionNode.cs @@ -0,0 +1,116 @@ +using Artemis.Core; +using Artemis.VisualScripting.Nodes.Maths.CustomViewModels; +using NoStringEvaluating.Contract; +using NoStringEvaluating.Contract.Variables; +using NoStringEvaluating.Models.Values; + +namespace Artemis.VisualScripting.Nodes.Maths; + +[Node("Math Expression", "Outputs the result of a math expression.", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class MathExpressionNode : Node +{ + private readonly INoStringEvaluator _evaluator; + private readonly PinsVariablesContainer _variables; + + #region Constructors + + public MathExpressionNode(INoStringEvaluator evaluator) + : base("Math Expression", "Outputs the result of a math expression.") + { + _evaluator = evaluator; + Output = CreateOutputPin(); + Values = CreateInputPinCollection("Values", 2); + Values.PinAdded += (_, _) => SetPinNames(); + Values.PinRemoved += (_, _) => SetPinNames(); + _variables = new PinsVariablesContainer(Values); + + SetPinNames(); + } + + #endregion + + #region Properties & Fields + + public OutputPin Output { get; } + public InputPinCollection Values { get; } + + #endregion + + #region Methods + + public override void Evaluate() + { + if (Storage != null) + Output.Value = new Numeric(_evaluator.CalcNumber(Storage, _variables)); + } + + private void SetPinNames() + { + int index = 1; + foreach (IPin value in Values) + { + value.Name = ExcelColumnFromNumber(index).ToLower(); + index++; + } + } + + public static string ExcelColumnFromNumber(int column) + { + string columnString = ""; + decimal columnNumber = column; + while (columnNumber > 0) + { + decimal currentLetterNumber = (columnNumber - 1) % 26; + char currentLetter = (char) (currentLetterNumber + 65); + columnString = currentLetter + columnString; + columnNumber = (columnNumber - (currentLetterNumber + 1)) / 26; + } + + return columnString; + } + + #endregion +} + +public class PinsVariablesContainer : IVariablesContainer +{ + private readonly InputPinCollection _values; + + public PinsVariablesContainer(InputPinCollection values) + { + _values = values; + } + + #region Implementation of IVariablesContainer + + /// + public IVariable AddOrUpdate(string name, double value) + { + throw new NotImplementedException(); + } + + /// + public EvaluatorValue GetValue(string name) + { + IPin pin = _values.FirstOrDefault(v => v.Name == name); + if (pin?.PinValue is Numeric numeric) + return new EvaluatorValue(numeric); + return new EvaluatorValue(0); + } + + /// + public bool TryGetValue(string name, out EvaluatorValue value) + { + IPin pin = _values.FirstOrDefault(v => v.Name == name); + if (pin?.PinValue is Numeric numeric) + { + value = new EvaluatorValue(numeric); + return true; + } + + value = new EvaluatorValue(0); + return false; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/RoundNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/RoundNode.cs new file mode 100644 index 000000000..ba081421f --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/RoundNode.cs @@ -0,0 +1,26 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes.Maths; + +[Node("Round", "Outputs a rounded numeric value.", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class RoundNode : Node +{ + public RoundNode() : base("Round", "Outputs a rounded numeric value.") + { + Input = CreateInputPin(); + Output = CreateOutputPin(); + } + + public OutputPin Output { get; set; } + public InputPin Input { get; set; } + + #region Overrides of Node + + /// + public override void Evaluate() + { + Output.Value = new Numeric(MathF.Round(Input.Value, MidpointRounding.AwayFromZero)); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/StaticValueNodes.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/StaticValueNodes.cs new file mode 100644 index 000000000..67fbef426 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/StaticValueNodes.cs @@ -0,0 +1,62 @@ +using Artemis.Core; +using Artemis.VisualScripting.Nodes.CustomViewModels; + +namespace Artemis.VisualScripting.Nodes; + +[Node("Numeric-Value", "Outputs a configurable static numeric value.", "Static", OutputType = typeof(Numeric))] +public class StaticNumericValueNode : Node +{ + #region Constructors + + public StaticNumericValueNode() + : base("Numeric", "Outputs a configurable numeric value.") + { + Output = CreateOutputPin(); + } + + #endregion + + #region Properties & Fields + + public OutputPin Output { get; } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = Storage; + } + + #endregion +} + +[Node("String-Value", "Outputs a configurable static string value.", "Static", OutputType = typeof(string))] +public class StaticStringValueNode : Node +{ + #region Constructors + + public StaticStringValueNode() + : base("String", "Outputs a configurable string value.") + { + Output = CreateOutputPin(); + } + + #endregion + + #region Properties & Fields + + public OutputPin Output { get; } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = Storage; + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/StringFormatNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/StringFormatNode.cs new file mode 100644 index 000000000..347312f20 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/StringFormatNode.cs @@ -0,0 +1,37 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes; + +[Node("Format", "Formats the input string.", "Text", InputType = typeof(object), OutputType = typeof(string))] +public class StringFormatNode : Node +{ + #region Constructors + + public StringFormatNode() + : base("Format", "Formats the input string.") + { + Format = CreateInputPin("Format"); + Values = CreateInputPinCollection("Values"); + Output = CreateOutputPin("Result"); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Output.Value = string.Format(Format.Value ?? string.Empty, Values.Values.ToArray()); + } + + #endregion + + #region Properties & Fields + + public InputPin Format { get; } + public InputPinCollection Values { get; } + + public OutputPin Output { get; } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/SumNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/SumNode.cs new file mode 100644 index 000000000..7cb49ebb5 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/SumNode.cs @@ -0,0 +1,35 @@ +using Artemis.Core; + +namespace Artemis.VisualScripting.Nodes; + +[Node("Sum", "Sums the connected numeric values.", "Mathematics", InputType = typeof(Numeric), OutputType = typeof(Numeric))] +public class SumNumericsNode : Node +{ + #region Constructors + + public SumNumericsNode() + : base("Sum", "Sums the connected numeric values.") + { + Values = CreateInputPinCollection("Values", 2); + Sum = CreateOutputPin("Sum"); + } + + #endregion + + #region Methods + + public override void Evaluate() + { + Sum.Value = Values.Values.Sum(); + } + + #endregion + + #region Properties & Fields + + public InputPinCollection Values { get; } + + public OutputPin Sum { get; } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/packages.lock.json b/src/Avalonia/Artemis.VisualScripting/packages.lock.json new file mode 100644 index 000000000..39d2fbeb4 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/packages.lock.json @@ -0,0 +1,1721 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "Avalonia": { + "type": "Direct", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "7st8nMai1C1nqw1a2H+zXiVYTnnfFwZz7JGziEzJK4sF6+x/W77XkdcDgDHyihcK3clQZJexYr4f+PzK4BJhSQ==", + "dependencies": { + "Avalonia.Remote.Protocol": "0.10.13", + "JetBrains.Annotations": "10.3.0", + "System.ComponentModel.Annotations": "4.5.0", + "System.Memory": "4.5.3", + "System.Reactive": "5.0.0", + "System.Runtime.CompilerServices.Unsafe": "4.6.0", + "System.ValueTuple": "4.5.0" + } + }, + "Avalonia.ReactiveUI": { + "type": "Direct", + "requested": "[0.10.13, )", + "resolved": "0.10.13", + "contentHash": "s5UUJ/MG97Jv9i+kxlgNSKx8Q6uJkgYMJ/LdOR3VM+7T32IRyhuxrNNCYygqk6avuJ4QqvMLU02T6v3GhI7WWA==", + "dependencies": { + "Avalonia": "0.10.13", + "ReactiveUI": "13.2.10", + "System.Reactive": "5.0.0" + } + }, + "Ninject": { + "type": "Direct", + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "CmbWW97FfJuh4LEOVZM/spqXl4KAulRUjqeMwRd5J9rDMQArmIYaDMU3pyzXXHT062tbF0OPIMwI7tSOtprPfg==", + "dependencies": { + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0" + } + }, + "NoStringEvaluating": { + "type": "Direct", + "requested": "[2.2.2, )", + "resolved": "2.2.2", + "contentHash": "hJHivPDA1Vxn0CCgOtHKZ3fmldxQuz7VL1J4lEaPTXCf+Vwcx1FDf05mGMh6olYMSxoKimGX8YK2sEoqeH3pnA==", + "dependencies": { + "Microsoft.Extensions.ObjectPool": "5.0.9" + } + }, + "ReactiveUI": { + "type": "Direct", + "requested": "[17.1.50, )", + "resolved": "17.1.50", + "contentHash": "UofZH1WMwWNLvFkK2SH+gsYTkUmhFFJO0Pix9YG2RzdHQ92mRFCzHzPO1abeU8/cxzyc9hJHX7sBChzUj53Ulg==", + "dependencies": { + "DynamicData": "7.5.2", + "Splat": "14.1.45" + } + }, + "SkiaSharp": { + "type": "Direct", + "requested": "[2.88.0-preview.178, )", + "resolved": "2.88.0-preview.178", + "contentHash": "arzd/44ykiBPqGWUuQqNTuJ49rhsXOg4Zw1p2Mm3B/5PZzV1wcTH4V+J+4ra8RS0KbIoy4KWeNF+zHAifNsiRg==", + "dependencies": { + "SkiaSharp.NativeAssets.Win32": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.macOS": "2.88.0-preview.178", + "System.Memory": "4.5.3" + } + }, + "Avalonia.Angle.Windows.Natives": { + "type": "Transitive", + "resolved": "2.1.0.2020091801", + "contentHash": "nGsCPI8FuUknU/e6hZIqlsKRDxClXHZyztmgM8vuwslFC/BIV3LqM2wKefWbr6SORX4Lct4nivhSMkdF/TrKgg==" + }, + "Avalonia.Controls.DataGrid": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "51xcaYtJN41vX/4xUu8rNyoISTO4bdswpfZmTPjeBTdofrhZ6mzOqbxVk6tqT4gt88MPihbaPil4jsD4X4Aixw==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Remote.Protocol": "0.10.13", + "JetBrains.Annotations": "10.3.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.Desktop": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "v+siRNQYvSZR9lt/bBgb81t6LGbSC7pUo+APgPmKYGLeYcMij1O6CWk7tCh9hihMxNHYw/PEB06r8ZBQIg9YPg==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Native": "0.10.13", + "Avalonia.Skia": "0.10.13", + "Avalonia.Win32": "0.10.13", + "Avalonia.X11": "0.10.13" + } + }, + "Avalonia.Diagnostics": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "stIGj0Rv/p/Re0GqlXCc061paifG6wT0YvrTUV/fQloNctW8Y4sf1xZNzr9dxdSz6+LG2AZjdZcSUhUGOCe6Zg==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Controls.DataGrid": "0.10.13", + "Microsoft.CodeAnalysis.CSharp.Scripting": "3.4.0", + "System.Reactive": "5.0.0" + } + }, + "Avalonia.FreeDesktop": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "/bKzscse/kY4RNMFk8r/jqLY/kS0fSkwp4TpqEF4UJeI8sHUvwe4yecnzNb1qDP9tCX4S6ML38LklAIqAk8gIQ==", + "dependencies": { + "Avalonia": "0.10.13", + "Tmds.DBus": "0.9.0" + } + }, + "Avalonia.Native": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "pq3WiiOyFyhJHnYdxP/fOlcG9DfqhJ0W5CCfPX48QyOdODbPgMF5LY6BU+McDpeAJTwQ4LqVfznHZoCeHH12gg==", + "dependencies": { + "Avalonia": "0.10.13" + } + }, + "Avalonia.Remote.Protocol": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "hnklCHyLCMrzWjMc3T0mYkRKdfUqpw2qCkf9HBRzyqnI6uG5tLw2QIlRF9zYC4BGOpx/B/647IcIjgq6H1ypzQ==" + }, + "Avalonia.Skia": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "aG0JlUhCpwGM/QsN/+rRak7XlPy0Jtd5HaiCdYKtuBOc+ISGs6hmCJDKjklNANp9gZR/TUUgXkqk5VFMQUJkTA==", + "dependencies": { + "Avalonia": "0.10.13", + "HarfBuzzSharp": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.Linux": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.WebAssembly": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.Linux": "2.88.0-preview.178", + "SkiaSharp.NativeAssets.WebAssembly": "2.88.0-preview.178" + } + }, + "Avalonia.Svg.Skia": { + "type": "Transitive", + "resolved": "0.10.12", + "contentHash": "qsXKdm5eWpfoVPe0xgtxhbOYlhG8QdbYNJZTTihg/c4iPFYuh1G7DldiNskuVFuGiGxLVZ0g6ebql7ZkwbO1pA==", + "dependencies": { + "Avalonia": "0.10.12", + "Avalonia.Skia": "0.10.12", + "SkiaSharp": "2.88.0-preview.178", + "Svg.Skia": "0.5.12" + } + }, + "Avalonia.Win32": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "CNUGWafAonBYYbHMliujDrs4JH2giH435GxU+O1q/nGyO5Mm+PXCG4NYsg+0zwp8yQBapFK7eYwzamU+re+cDw==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Angle.Windows.Natives": "2.1.0.2020091801", + "System.Drawing.Common": "4.5.0", + "System.Numerics.Vectors": "4.5.0" + } + }, + "Avalonia.X11": { + "type": "Transitive", + "resolved": "0.10.13", + "contentHash": "kXxn79KVB0ZfeZqQL7c2Dlvl96GBlRT8rzAh6g/j0hcgykQ55/e0be8Te6+Ny7hI+tFrob6lxvYdxYVUUCjHYg==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.FreeDesktop": "0.10.13", + "Avalonia.Skia": "0.10.13" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "1TtKHYYVfox7aUZ0akCqkULmAjpG8X5ZRzTzTiONY34xtvvaPuUSSdVL1VaF/1/ljRhOkpy+uKOGn6XoFGvorw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "DynamicData": { + "type": "Transitive", + "resolved": "7.5.4", + "contentHash": "1OpHPoyQGzHREiP6JXnPaBBx4KWVQZW7zBAZpKXc9kl4rcbEK4fo2/T3bDXZbHWKhDqVAISW9pE4Ug9+ms3RoA==", + "dependencies": { + "System.Reactive": "5.0.0" + } + }, + "EmbedIO": { + "type": "Transitive", + "resolved": "3.4.3", + "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "dependencies": { + "Unosquare.Swan.Lite": "3.0.0" + } + }, + "Fizzler": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "CPxuWF8EPvM0rwAtMTR5G+7EuLoGNXsEfqyx06upN9JyVALZ73KgbGn3SLFwGosifiUAXrvNHtXlUwGGytdECg==" + }, + "FluentAvaloniaUI": { + "type": "Transitive", + "resolved": "1.3.0", + "contentHash": "xzcsuOswakMpz/EdA59NEOgaCtZ/9zsd5QWTB0YYQqSv1GF95Uk2aMVtO5gtfNrCT4lZvGNWVf3HGjYz9cHovQ==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Desktop": "0.10.13", + "Avalonia.Diagnostics": "0.10.13", + "MicroCom.CodeGenerator.MSBuild": "0.10.4", + "MicroCom.Runtime": "0.10.4" + } + }, + "HarfBuzzSharp": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "OUir5qn95QRtlc8RWKfU/63xYwtuAbylL2oAj3eBWgAsVoWnFrEv+Oh1sj0xjW7mogFGaeGtY40lqAD1srWJcQ==", + "dependencies": { + "HarfBuzzSharp.NativeAssets.Win32": "2.8.2-preview.178", + "HarfBuzzSharp.NativeAssets.macOS": "2.8.2-preview.178", + "System.Memory": "4.5.3" + } + }, + "HarfBuzzSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "4scihdELcRpCEubBsUMHUJn93Xvx6ASj596WfO9y8CEuFNW0LBMDL71HBCyq5zXsn8HyGjLtoBLW0PpXbVnpjQ==", + "dependencies": { + "HarfBuzzSharp": "2.8.2-preview.178" + } + }, + "HarfBuzzSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "QtmAs62il4vFtt3fFOXhhPDl7TX+NGu4tFB5qmnqUn+EnSJW7mxqNk1n9I7+Z2ORym0nTP4dhcRNtOpOS7Oenw==" + }, + "HarfBuzzSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "+S8qtBAVTrt+E85jZXPxYthUgSUq7iB6UZ0v0WFsy9gWhZ/hVE3hZJpcgeywT9H/SRX3ZIX+qzpKJlOM+mUcNA==" + }, + "HarfBuzzSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.8.2-preview.178", + "contentHash": "EgeF5uCZcAriIHmWq3hKNxz/jBJLeP/PKU4yI87UNkJCt4hYignOMjY0irl/rGVZtTL/G05xxf7TB6sjisi8sQ==" + }, + "HidSharp": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UTdxWvbgp2xzT1Ajaa2va+Qi3oNHJPasYmVhbKI2VVdu1VYP6yUG+RikhsHvpD7iM0S8e8UYb5Qm/LTWxx9QAA==" + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.11.10", + "contentHash": "4TBsHSXPocdsEB5dewIHeKykTzIz5Ui7ouXw4JsUGI+ax4jjviVJVD7+gsPCNyA+b3de2EjYI+jcEq8I/1ZFSQ==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "0GLU9lwGVXjUNlr9ZIdAgjqLI2Zm/XFGJFaqJ1T1sU+kwfeMLhm68+rblUrNUP9psRl4i8yM7Ghb4ia4oI2E5g==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "LiteDB": { + "type": "Transitive", + "resolved": "5.0.11", + "contentHash": "6cL4bOmVCUB0gIK+6qIr68HeqjjHZicPDGQjvJ87mIOvkFsEsJWkIps3yoKNeLpHhJQur++yoQ9Q8gxsdos0xQ==" + }, + "Material.Icons": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "4UIT91QbedjNfUYU+R3T60U+InozFtIsP1iUzGbkq/G0f1eDE3tXMWUuLEDO3yCEP2MHrPjAOpokwqk1rnWNGA==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + }, + "Material.Icons.Avalonia": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "hK0MQm2XyPwjT+DiviDOBjrJVQe6V0u+XTDVbxohkq58hUBlq0XZXmHHZ27jUJU6ZVP9ybu44aXfWycbVjnY2A==", + "dependencies": { + "Avalonia": "0.10.0", + "Material.Icons": "1.0.2" + } + }, + "McMaster.NETCore.Plugins": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "UKw5Z2/QHhkR7kiAJmqdCwVDMQV0lwsfj10+FG676r8DsJWIpxtachtEjE0qBs9WoK5GUQIqxgyFeYUSwuPszg==", + "dependencies": { + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.Extensions.DependencyModel": "5.0.0" + } + }, + "MicroCom.CodeGenerator.MSBuild": { + "type": "Transitive", + "resolved": "0.10.4", + "contentHash": "aG1kLtkgX6lC8qpxVon4OFSCdWYEbQubIg+2/ychWTIFTrDHWFkhcC4YTn0IfGiVCLwh0Yj7eSc8nk5f3UoMKg==" + }, + "MicroCom.Runtime": { + "type": "Transitive", + "resolved": "0.10.4", + "contentHash": "enc2U+/1UnF3rtocxb5ofcg7cJSmJI4adbYPr8DZa5bQzvhqA/VbjlcalxoqjI3CR2RvM5WWpjKT0p3BriFJjw==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "2.9.6", + "contentHash": "Kmms3TxGQMNb95Cu/3K+0bIcMnV4qf/phZBLAB0HUi65rBPxP4JO3aM2LoAcb+DFS600RQJMZ7ZLyYDTbLwJOQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "3ncA7cV+iXGA1VYwe2UEZXcvWyZSlbexWjM9AvocP7sik5UD93qt9Hq0fMRGk0jFRmvmE4T2g+bGfXiBVZEhLw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "2.9.6", + "System.Collections.Immutable": "1.5.0", + "System.Memory": "4.5.3", + "System.Reflection.Metadata": "1.6.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.2", + "System.Text.Encoding.CodePages": "4.5.1", + "System.Threading.Tasks.Extensions": "4.5.3" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "/LsTtgcMN6Tu1oo7/WYbRAHL4/ubXC/miEakwTpcZKJKtFo7D0AK95Hw0dbGxul6C8WJu60v6NP2435TDYZM+Q==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Scripting": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "tLgqc76qXHmONUhWhxo7z3TcL/LmGFWIUJm1exbQmVJohuQvJnejUMxmVkdxDfMuMZU1fIyJXPZ6Fkp4FEneAg==", + "dependencies": { + "Microsoft.CSharp": "4.3.0", + "Microsoft.CodeAnalysis.CSharp": "[3.4.0]", + "Microsoft.CodeAnalysis.Common": "[3.4.0]", + "Microsoft.CodeAnalysis.Scripting.Common": "[3.4.0]" + } + }, + "Microsoft.CodeAnalysis.Scripting.Common": { + "type": "Transitive", + "resolved": "3.4.0", + "contentHash": "+b6I3DZL2zvck+B/E/aiOveakj5U2G2BcYODQxcGh2IDbatNU3XXxGT1HumkWB5uIZI2Leu0opBgBpjScmjGMA==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[3.4.0]" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P+MBhIM0YX+JqROuf7i306ZLJEjQYA9uUyRDE+OqwUI5sh41e2ZbPQV3LfAPh+29cmceE1pUffXsGfR4eMY3KA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "umBECCoMC+sOUgm083yFr8SxTobUOcPFH4AXigdO2xJiszCHAnmeDl4qPphJt+oaJ/XIfV1wOjIts2nRnki61Q==" + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "5.0.9", + "contentHash": "grj0e6Me0EQsgaurV0fxP0xd8sz8eZVK+Jb816DPzNADHaqXaXJD3xZX9SFjyDl3ykAYvD0y77o5vRd9Hzsk9g==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "LuI1oG+24TUj1ZRQQjM5Ew73BKnZE5NZ/7eAdh1o8ST5dPhUnJvIkiIn2re3MwnkRy6ELRnvEbBxHP8uALKhJw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Ninject.Extensions.ChildKernel": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "vl/p3f8sIaCCHiKsjhq9R8n3bH705Hu1WJXNpMEz1UC79EV51Mk5TWYXQbRnsK20hxF48CiAgUBb9pMKfX6sLw==", + "dependencies": { + "Ninject": "3.3.4" + } + }, + "Ninject.Extensions.Conventions": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "bAMK7tRHIRQ+gjR1WxwTlNuP+/bKRIFf6NKObkWP3XVzFQhsLEKA0hEo73OXuBdpng0jczhqCGmwu630nIa/bg==", + "dependencies": { + "Ninject.Extensions.Factory": "3.3.2" + } + }, + "Ninject.Extensions.Factory": { + "type": "Transitive", + "resolved": "3.3.2", + "contentHash": "H9s77i9WsbgF6s7OieQ+c51KoW90jJAQqb0ClEqi6SBtL7jySUjh/5HCjnYgyQ8iYcWhvhw9cFnYxX9CB1kL7Q==", + "dependencies": { + "Castle.Core": "4.2.0", + "Ninject": "3.3.3" + } + }, + "ReactiveUI.Validation": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "rhEphZ4ErbGfNtbBQ/tYMsLJYHyLVyqidU+sgZ3kXKbS7QrNoM4j6PPxCwLMKsJUuvVL8JN45xgmB9tSwm7+lg==", + "dependencies": { + "ReactiveUI": "16.2.6" + } + }, + "RGB.NET.Core": { + "type": "Transitive", + "resolved": "1.0.0-prerelease7", + "contentHash": "IIja5sC4QZ5pbSNckRCG7TlY4U6j/dRbrl4e2FZqsTGgsevaVB3IqonUQLFY1GGst4xNSl2oh0A23coXQxXGbQ==" + }, + "RGB.NET.Layout": { + "type": "Transitive", + "resolved": "1.0.0-prerelease7", + "contentHash": "S0kfWVa8EfMOAl2WPHsq98dwaO+SNz9TWr1AtMkdo8aZuYIVhaJ1c+mSAMMnH1V+mSbxDWPHWkNzi9ITszJucA==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease7" + } + }, + "RGB.NET.Presets": { + "type": "Transitive", + "resolved": "1.0.0-prerelease7", + "contentHash": "NgShvOPQM0miOsdqMKjkNunngJUZMwr8KR8ME2/Ksir7wgIQfgJj1YwZy8aIj+ar7fDo6VZJZenAshs/Ul+04A==", + "dependencies": { + "RGB.NET.Core": "1.0.0-prerelease7" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "2.10.0", + "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA==" + }, + "Serilog.Sinks.Console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "apLOvSJQLlIbKlbx+Y2UDHSP05kJsV7mou+fvJoRGs/iR+jC22r8cuFVMjjfVxz/AD4B2UCltFhE1naRLXwKNw==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "ShimSkiaSharp": { + "type": "Transitive", + "resolved": "0.5.12", + "contentHash": "oUGM7gQHRzbGPRs3E1pe5e8VwML21YyEz9xdo+r2ov1mAqSDPyXErVQP6pN4gnfYMVf5ADR7BVkVzt4R9Iz3gQ==" + }, + "SkiaSharp.HarfBuzz": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "qTf3PbRJsTVOttQkYWOCswa1QnkH0dEOqMSgr3iXZQuKPNlj1qpGY8+OGPs25WKgUEqOpv2nog/AYQ/bpyOXzA==", + "dependencies": { + "HarfBuzzSharp": "2.8.2-preview.178", + "SkiaSharp": "2.88.0-preview.178" + } + }, + "SkiaSharp.NativeAssets.Linux": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "nc9C8zGvL2G7p0lcTPhN4EOt2Mozv6KLJinMwjF97sYoI5cpkXCPZSRTcyf8k49gAZaOd+UMGaygCAz/8vaaWg==", + "dependencies": { + "SkiaSharp": "2.88.0-preview.178" + } + }, + "SkiaSharp.NativeAssets.macOS": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "+Hs3ku4buimzBHuc8FoyjOcE6eU5r98zcG7WH/s+doYQ1bFIjk+dKfqthgZ2o0NRAv8D3esq9rWrZTj12q+m1w==" + }, + "SkiaSharp.NativeAssets.WebAssembly": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "u4Ss81oOlx0dhu5fxl4vK5f2Hm7psHDUSAoQValNV/BmixsW4TkETE3dOnHNRWwI56++tRG9dK33HimZDUrUpw==" + }, + "SkiaSharp.NativeAssets.Win32": { + "type": "Transitive", + "resolved": "2.88.0-preview.178", + "contentHash": "9og9GCkdZc/NYrmbsRzohmIBRlS1oFegJiBMsoG93qYjhh2o6q5QBYxd61zw5Mgeytl3qj4YM+6BNIF4tcF+6w==" + }, + "Splat": { + "type": "Transitive", + "resolved": "14.1.45", + "contentHash": "ayHdfTUklD5ci0s9m4uYMccjtkKVjZ9fVPT5q3PN+SnvyD6bjQVRozOfUHwdwh4LAz9ETZjR/tAgrm+IapXKrw==" + }, + "Svg.Custom": { + "type": "Transitive", + "resolved": "0.5.12", + "contentHash": "kmjLQf5U5WC7tRGBedUhtrOUCR0NaNL2auzOA2a/oMwEA0Bjrpd6qvMTpJUS3HITxi8vJazGl270K+i0JvdJog==", + "dependencies": { + "Fizzler": "1.2.0", + "System.Memory": "4.5.3", + "System.ObjectModel": "4.3.0", + "System.ValueTuple": "4.5.0" + } + }, + "Svg.Model": { + "type": "Transitive", + "resolved": "0.5.12", + "contentHash": "/CPiXIugg4oVyYlQr26fB1X9iQfICALF8AJXbTWnXGoP2WZa1t6aZbAXPk3HoPApA0w5waf3XXkBiYYnWwawaQ==", + "dependencies": { + "ShimSkiaSharp": "0.5.12", + "Svg.Custom": "0.5.12" + } + }, + "Svg.Skia": { + "type": "Transitive", + "resolved": "0.5.12", + "contentHash": "KjKpjz0FKge+WpRzjD1bqywAW3vZhXwpR5c7Ej5OuP4xDrQjBwtFeB0iZ+yEJMzwXf/Rs4ImuN8m3bmBDJvMHg==", + "dependencies": { + "SkiaSharp": "2.88.0-preview.178", + "SkiaSharp.HarfBuzz": "2.88.0-preview.178", + "Svg.Custom": "0.5.12", + "Svg.Model": "0.5.12" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "EXKiDFsChZW0RjrZ4FYHu9aW6+P4MCgEDCklsVseRfhoO0F+dXeMSsMRAlVXIo06kGJ/zv+2w1a2uc2+kxxSaQ==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "UxYQ3FGUOtzJ7LfSdnYSFd7+oEv6M8NgUatatIN2HxNtDdlcvFAf+VIq4Of9cDMJEJC0aSRv/x898RYhB4Yppg==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "AiJFxxVPdeITstiRS5aAu8+8Dpf5NawTMoapZ53Gfirml24p7HIfhjmCRxdXnmmf3IUA3AX3CcW7G73CjWxW/Q==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0", + "Microsoft.Win32.SystemEvents": "4.5.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5NecZgXktdGg34rh1OenY1rFNDCI8xSjFr+Z4OU4cU06AQHUdRnIIEeWENu3Wl4YowbzkymAIMvi3WyK9U53pQ==" + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "HxozeSlipUK7dAroTYwIcGwKDeOVpQnJlpVaOkBz7CM4TsE5b/tKlQBZecTjh6FzcSbxndYaxxpsBMz+wMJeyw==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "4J2JQXbftjPMppIHJ7IC+VXQ9XfEagN92vZZNoG12i+zReYlim5dMoXFC1Zzg7tsnKDM7JPo5bYfFK4Jheq44w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.1.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "+MvhNtcvIbqmhANyKu91jQnvIRVSTiaOiFNfKWwXGHG48YAb4I/TyH8spsySiPYla7gKal5ZnF3teJqZAximyQ==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "Tmds.DBus": { + "type": "Transitive", + "resolved": "0.9.0", + "contentHash": "KcTWL9aKuob9Qo2sOTTKFePs1rKGTwZrcBvMFuGVIVR5RojX3oIFj5UBLYfSGjYgrcImC7LjQI3DdCFwUnhNXw==", + "dependencies": { + "System.Reflection.Emit": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, + "Unosquare.Swan.Lite": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" + }, + "artemis.core": { + "type": "Project", + "dependencies": { + "Artemis.Storage": "1.0.0", + "EmbedIO": "3.4.3", + "HidSharp": "2.1.0", + "Humanizer.Core": "2.11.10", + "LiteDB": "5.0.11", + "McMaster.NETCore.Plugins": "1.4.0", + "Newtonsoft.Json": "13.0.1", + "Ninject": "3.3.4", + "Ninject.Extensions.ChildKernel": "3.3.0", + "Ninject.Extensions.Conventions": "3.3.0", + "RGB.NET.Core": "1.0.0-prerelease7", + "RGB.NET.Layout": "1.0.0-prerelease7", + "RGB.NET.Presets": "1.0.0-prerelease7", + "Serilog": "2.10.0", + "Serilog.Sinks.Console": "4.0.1", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.0.0", + "SkiaSharp": "2.88.0-preview.178", + "System.Buffers": "4.5.1", + "System.IO.FileSystem.AccessControl": "5.0.0", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.Metadata": "5.0.0", + "System.ValueTuple": "4.5.0" + } + }, + "artemis.storage": { + "type": "Project", + "dependencies": { + "LiteDB": "5.0.11", + "Serilog": "2.10.0" + } + }, + "artemis.ui.shared": { + "type": "Project", + "dependencies": { + "Artemis.Core": "1.0.0", + "Avalonia": "0.10.13", + "Avalonia.ReactiveUI": "0.10.13", + "Avalonia.Svg.Skia": "0.10.12", + "DynamicData": "7.5.4", + "FluentAvaloniaUI": "1.3.0", + "Material.Icons.Avalonia": "1.0.2", + "RGB.NET.Core": "1.0.0-prerelease7", + "ReactiveUI": "17.1.50", + "ReactiveUI.Validation": "2.2.1", + "SkiaSharp": "2.88.0-preview.178" + } + } + } + } +} \ No newline at end of file From 885cd852fc167a6c78071ac13bb9613e15f62063 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 14 Mar 2022 23:45:21 +0100 Subject: [PATCH 147/270] Node editor - Added node selection --- .../Controls/SelectionRectangle.cs | 24 +++---- .../Ninject/Factories/IVMFactory.cs | 2 +- .../VisualScripting/NodeScriptView.axaml | 19 +++++- .../VisualScripting/NodeScriptView.axaml.cs | 27 ++++++++ .../VisualScripting/NodeScriptViewModel.cs | 68 ++++++++++++++++--- .../Screens/VisualScripting/NodeView.axaml | 19 +++++- .../Screens/VisualScripting/NodeView.axaml.cs | 42 ++++++++++++ .../Screens/VisualScripting/NodeViewModel.cs | 37 ++++++++-- 8 files changed, 205 insertions(+), 33 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index c51659c1b..09e102ae0 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -166,23 +166,20 @@ public class SelectionRectangle : Control ((SelectionRectangle) sender).SubscribeToInputElement(); } - - private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) + private void ParentOnPointerMoved(object? sender, PointerEventArgs e) { if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; - e.Pointer.Capture(this); - - _startPosition = e.GetPosition(Parent); - _absoluteStartPosition = e.GetPosition(VisualRoot); - _displayRect = null; - } - - private void ParentOnPointerMoved(object? sender, PointerEventArgs e) - { + // Capture the pointer and initialize dragging the first time it moves if (!ReferenceEquals(e.Pointer.Captured, this)) - return; + { + e.Pointer.Capture(this); + + _startPosition = e.GetPosition(Parent); + _absoluteStartPosition = e.GetPosition(VisualRoot); + _displayRect = null; + } Point currentPosition = e.GetPosition(Parent); Point absoluteCurrentPosition = e.GetPosition(VisualRoot); @@ -223,7 +220,6 @@ public class SelectionRectangle : Control { if (_oldInputElement != null) { - _oldInputElement.PointerPressed -= ParentOnPointerPressed; _oldInputElement.PointerMoved -= ParentOnPointerMoved; _oldInputElement.PointerReleased -= ParentOnPointerReleased; } @@ -232,7 +228,6 @@ public class SelectionRectangle : Control if (InputElement != null) { - InputElement.PointerPressed += ParentOnPointerPressed; InputElement.PointerMoved += ParentOnPointerMoved; InputElement.PointerReleased += ParentOnPointerReleased; } @@ -259,7 +254,6 @@ public class SelectionRectangle : Control { if (_oldInputElement != null) { - _oldInputElement.PointerPressed -= ParentOnPointerPressed; _oldInputElement.PointerMoved -= ParentOnPointerMoved; _oldInputElement.PointerReleased -= ParentOnPointerReleased; _oldInputElement = null; diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 652feee62..140914580 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -94,7 +94,7 @@ namespace Artemis.UI.Ninject.Factories { NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript); NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); - NodeViewModel NodeViewModel(NodeScript nodeScript, INode node); + NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node); } public interface INodePinVmFactory diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml index fd189eb1d..b08cf9b2c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" + xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptView" x:DataType="visualScripting:NodeScriptViewModel"> @@ -23,7 +24,10 @@ VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="{DynamicResource LargeCheckerboardBrush}" - ZoomChanged="ZoomBorder_OnZoomChanged"> + ZoomChanged="ZoomBorder_OnZoomChanged" + MaxZoomX="1" + MaxZoomY="1" + PointerReleased="ZoomBorder_OnPointerReleased"> @@ -52,7 +56,7 @@ - + @@ -65,6 +69,17 @@ + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index c3094011b..9ace2bbf8 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -1,6 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.UI.Shared.Controls; +using Artemis.UI.Shared.Events; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Generators; using Avalonia.Controls.PanAndZoom; using Avalonia.Input; using Avalonia.Interactivity; @@ -15,13 +20,17 @@ namespace Artemis.UI.Screens.VisualScripting { private readonly ZoomBorder _zoomBorder; private readonly Grid _grid; + private readonly ItemsControl _nodesContainer; + private readonly SelectionRectangle _selectionRectangle; public NodeScriptView() { InitializeComponent(); + _nodesContainer = this.Find("NodesContainer"); _zoomBorder = this.Find("ZoomBorder"); _grid = this.Find("ContainerGrid"); + _selectionRectangle = this.Find("SelectionRectangle"); _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; UpdateZoomBorderBackground(); @@ -56,5 +65,23 @@ namespace Artemis.UI.Screens.VisualScripting { UpdateZoomBorderBackground(); } + + private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) + { + List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); + List nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList(); + ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + } + + private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) + { + ViewModel?.FinishNodeSelection(); + } + + private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_selectionRectangle.IsSelecting) + ViewModel?.ClearNodeSelection(); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 88caf49a6..26f42fd97 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -1,10 +1,10 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Events; -using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; @@ -16,19 +16,16 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptViewModel : ActivatableViewModelBase { - private readonly INodeService _nodeService; - private readonly INodeEditorService _nodeEditorService; private readonly INodeVmFactory _nodeVmFactory; + private List? _initialNodeSelection; - public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService) + public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) { _nodeVmFactory = nodeVmFactory; - _nodeService = nodeService; - _nodeEditorService = nodeEditorService; NodeScript = nodeScript; NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); - History = _nodeEditorService.GetHistory(NodeScript); + History = nodeEditorService.GetHistory(NodeScript); this.WhenActivated(d => { @@ -42,7 +39,9 @@ public class NodeScriptViewModel : ActivatableViewModelBase NodeViewModels = new ObservableCollection(); foreach (INode nodeScriptNode in NodeScript.Nodes) - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, nodeScriptNode)); + NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); + + CableViewModels = new ObservableCollection(); } public NodeScript NodeScript { get; } @@ -51,9 +50,60 @@ public class NodeScriptViewModel : ActivatableViewModelBase public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } + public void UpdateNodeSelection(List nodes, bool expand, bool invert) + { + _initialNodeSelection ??= NodeViewModels.Where(vm => vm.IsSelected).ToList(); + + if (expand) + { + foreach (NodeViewModel nodeViewModel in nodes) + nodeViewModel.IsSelected = true; + } + else if (invert) + { + foreach (NodeViewModel nodeViewModel in nodes) + nodeViewModel.IsSelected = !_initialNodeSelection.Contains(nodeViewModel); + } + else + { + foreach (NodeViewModel nodeViewModel in nodes) + nodeViewModel.IsSelected = true; + foreach (NodeViewModel nodeViewModel in NodeViewModels.Except(nodes)) + nodeViewModel.IsSelected = false; + } + } + + public void FinishNodeSelection() + { + _initialNodeSelection = null; + } + + public void ClearNodeSelection() + { + foreach (NodeViewModel nodeViewModel in NodeViewModels) + nodeViewModel.IsSelected = false; + } + + public void StartNodeDrag(Point position) + { + foreach (NodeViewModel nodeViewModel in NodeViewModels) + nodeViewModel.SaveDragOffset(position); + } + + public void UpdateNodeDrag(Point position) + { + foreach (NodeViewModel nodeViewModel in NodeViewModels) + nodeViewModel.UpdatePosition(position); + } + + public void FinishNodeDrag() + { + // TODO: Command + } + private void HandleNodeAdded(SingleValueEventArgs eventArgs) { - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, eventArgs.Value)); + NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); } private void HandleNodeRemoved(SingleValueEventArgs eventArgs) diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index a3ce0fbda..33b2bb1b2 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -13,14 +13,29 @@ + + + + + + + + + - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index 9244d4791..6a3cf384a 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -1,12 +1,18 @@ using System; +using System.Collections.Generic; using Avalonia; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; namespace Artemis.UI.Screens.VisualScripting; public class NodeView : ReactiveUserControl { + private bool _dragging; + public NodeView() { InitializeComponent(); @@ -33,4 +39,40 @@ public class NodeView : ReactiveUserControl { AvaloniaXamlLoader.Load(this); } + + private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (ViewModel == null || e.InitialPressMouseButton != MouseButton.Left) + return; + + if (_dragging) + { + _dragging = false; + ViewModel.NodeScriptViewModel.FinishNodeDrag(); + e.Pointer.Capture(null); + return; + } + + ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + ViewModel.NodeScriptViewModel.FinishNodeSelection(); + + e.Handled = true; + } + + private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e) + { + PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType()); + if (ViewModel == null || !point.Properties.IsLeftButtonPressed) + return; + + if (!_dragging) + { + _dragging = true; + ViewModel.NodeScriptViewModel.StartNodeDrag(point.Position); + e.Pointer.Capture((IInputElement?) sender); + } + ViewModel.NodeScriptViewModel.UpdateNodeDrag(point.Position); + + e.Handled = true; + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 07be3a158..13eb8c500 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -4,10 +4,12 @@ using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Avalonia; using Avalonia.Controls.Mixins; using DynamicData; using ReactiveUI; @@ -16,16 +18,18 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeViewModel : ActivatableViewModelBase { - private readonly NodeScript _nodeScript; private readonly INodeEditorService _nodeEditorService; private ICustomNodeViewModel? _customNodeViewModel; private ReactiveCommand? _deleteNode; private ObservableAsPropertyHelper? _isStaticNode; + private double _dragOffsetX; + private double _dragOffsetY; + private bool _isSelected; - public NodeViewModel(NodeScript nodeScript, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) + public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { - _nodeScript = nodeScript; + NodeScriptViewModel = nodeScriptViewModel; _nodeEditorService = nodeEditorService; Node = node; @@ -54,6 +58,7 @@ public class NodeViewModel : ActivatableViewModelBase public bool IsStaticNode => _isStaticNode?.Value ?? true; + public NodeScriptViewModel NodeScriptViewModel { get; set; } public INode Node { get; } public ReadOnlyObservableCollection InputPinViewModels { get; } public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } @@ -72,8 +77,32 @@ public class NodeViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _deleteNode, value); } + public bool IsSelected + { + get => _isSelected; + set => RaiseAndSetIfChanged(ref _isSelected, value); + } + + public void SaveDragOffset(Point mouseStartPosition) + { + if (!IsSelected) + return; + + _dragOffsetX = Node.X - mouseStartPosition.X; + _dragOffsetY = Node.Y - mouseStartPosition.Y; + } + + public void UpdatePosition(Point mousePosition) + { + if (!IsSelected) + return; + + Node.X = Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10d; + Node.Y = Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10d; + } + private void ExecuteDeleteNode() { - _nodeEditorService.ExecuteCommand(_nodeScript, new DeleteNode(_nodeScript, Node)); + _nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node)); } } \ No newline at end of file From 86b4258f5d78a3dae361e34ca1e87aa6cc959cc9 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 18 Mar 2022 23:51:54 +0100 Subject: [PATCH 148/270] Node editor - Added cables --- src/Artemis.Core/VisualScripting/Pin.cs | 9 + .../Ninject/Factories/IVMFactory.cs | 172 +++++++++--------- .../Extensions/SKColorExtensions.cs | 20 ++ .../Services/NodeEditor/Commands/MoveNode.cs | 18 ++ .../ColorToSolidColorBrushConverter.cs | 11 +- .../Ninject/Factories/IVMFactory.cs | 5 +- .../Screens/VisualScripting/CableView.axaml | 37 +++- .../VisualScripting/CableView.axaml.cs | 32 ++-- .../Screens/VisualScripting/CableViewModel.cs | 112 +++++++++++- .../VisualScripting/NodeScriptView.axaml | 10 +- .../VisualScripting/NodeScriptViewModel.cs | 56 +++++- .../Screens/VisualScripting/NodeView.axaml | 4 +- .../Screens/VisualScripting/NodeView.axaml.cs | 20 +- .../Screens/VisualScripting/NodeViewModel.cs | 72 ++++++-- .../Pins/InputPinCollectionView.axaml | 15 +- .../Pins/InputPinCollectionViewModel.cs | 3 +- .../VisualScripting/Pins/InputPinView.axaml | 2 +- .../Pins/InputPinView.axaml.cs | 66 +++++++ .../Pins/OutputPinCollectionView.axaml | 15 +- .../Pins/OutputPinCollectionViewModel.cs | 3 +- .../Pins/OutputPinView.axaml.cs | 32 +++- .../Pins/PinCollectionViewModel.cs | 43 ++++- .../VisualScripting/Pins/PinViewModel.cs | 46 ++++- .../VisualScripting/VisualScripting.axaml | 1 + .../Screens/Workshop/WorkshopViewModel.cs | 14 +- 25 files changed, 655 insertions(+), 163 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs diff --git a/src/Artemis.Core/VisualScripting/Pin.cs b/src/Artemis.Core/VisualScripting/Pin.cs index 285415286..0407014eb 100644 --- a/src/Artemis.Core/VisualScripting/Pin.cs +++ b/src/Artemis.Core/VisualScripting/Pin.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using Artemis.Core.Events; namespace Artemis.Core @@ -81,6 +82,8 @@ namespace Artemis.Core OnPropertyChanged(nameof(ConnectedTo)); PinConnected?.Invoke(this, new SingleValueEventArgs(pin)); + if (!pin.ConnectedTo.Contains(this)) + pin.ConnectTo(this); } /// @@ -90,6 +93,8 @@ namespace Artemis.Core OnPropertyChanged(nameof(ConnectedTo)); PinDisconnected?.Invoke(this, new SingleValueEventArgs(pin)); + if (pin.ConnectedTo.Contains(this)) + pin.DisconnectFrom(this); } /// @@ -101,7 +106,11 @@ namespace Artemis.Core OnPropertyChanged(nameof(ConnectedTo)); foreach (IPin pin in connectedPins) + { PinDisconnected?.Invoke(this, new SingleValueEventArgs(pin)); + if (pin.ConnectedTo.Contains(this)) + pin.DisconnectFrom(this); + } } #endregion diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index dfd2a6b42..b7061e837 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -6,7 +6,6 @@ using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Event; using Artemis.UI.Screens.ProfileEditor.DisplayConditions.Static; using Artemis.UI.Screens.ProfileEditor.LayerProperties; -using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree; @@ -26,107 +25,106 @@ using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit; using Stylet; -namespace Artemis.UI.Ninject.Factories +namespace Artemis.UI.Ninject.Factories; + +public interface IVmFactory { - public interface IVmFactory - { - } +} - public interface ISettingsVmFactory : IVmFactory - { - PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); - PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); - DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); - } +public interface ISettingsVmFactory : IVmFactory +{ + PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); + PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); + DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); +} - public interface IDeviceDebugVmFactory : IVmFactory - { - DeviceDialogViewModel DeviceDialogViewModel(ArtemisDevice device); - DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); - DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); - DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device); - InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device); - } +public interface IDeviceDebugVmFactory : IVmFactory +{ + DeviceDialogViewModel DeviceDialogViewModel(ArtemisDevice device); + DevicePropertiesTabViewModel DevicePropertiesTabViewModel(ArtemisDevice device); + DeviceInfoTabViewModel DeviceInfoTabViewModel(ArtemisDevice device); + DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device); + InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device); +} - public interface IProfileTreeVmFactory : IVmFactory - { - FolderViewModel FolderViewModel(ProfileElement folder); - LayerViewModel LayerViewModel(ProfileElement layer); - } +public interface IProfileTreeVmFactory : IVmFactory +{ + FolderViewModel FolderViewModel(ProfileElement folder); + LayerViewModel LayerViewModel(ProfileElement layer); +} - public interface ILayerHintVmFactory : IVmFactory - { - LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer); - CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint); - DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint); - KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint); - } +public interface ILayerHintVmFactory : IVmFactory +{ + LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer); + CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint); + DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint); + KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint); +} - public interface IHeaderVmFactory : IVmFactory - { - SimpleHeaderViewModel SimpleHeaderViewModel(string displayName); - } +public interface IHeaderVmFactory : IVmFactory +{ + SimpleHeaderViewModel SimpleHeaderViewModel(string displayName); +} - public interface IProfileLayerVmFactory : IVmFactory - { - ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel); - } +public interface IProfileLayerVmFactory : IVmFactory +{ + ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel); +} - public interface IVisualizationToolVmFactory : IVmFactory - { - ViewpointMoveToolViewModel ViewpointMoveToolViewModel(PanZoomViewModel panZoomViewModel); - EditToolViewModel EditToolViewModel(PanZoomViewModel panZoomViewModel); - SelectionToolViewModel SelectionToolViewModel(PanZoomViewModel panZoomViewModel); - SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel); - } +public interface IVisualizationToolVmFactory : IVmFactory +{ + ViewpointMoveToolViewModel ViewpointMoveToolViewModel(PanZoomViewModel panZoomViewModel); + EditToolViewModel EditToolViewModel(PanZoomViewModel panZoomViewModel); + SelectionToolViewModel SelectionToolViewModel(PanZoomViewModel panZoomViewModel); + SelectionRemoveToolViewModel SelectionRemoveToolViewModel(PanZoomViewModel panZoomViewModel); +} - public interface ILayerPropertyVmFactory : IVmFactory - { - LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty); +public interface ILayerPropertyVmFactory : IVmFactory +{ + LayerPropertyViewModel LayerPropertyViewModel(ILayerProperty layerProperty); - LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); - TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); - TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); + LayerPropertyGroupViewModel LayerPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); + TreeGroupViewModel TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); + TimelineGroupViewModel TimelineGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel); - TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); - EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel); - TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); - TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups); - } + TreeViewModel TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); + EffectsViewModel EffectsViewModel(LayerPropertiesViewModel layerPropertiesViewModel); + TimelineViewModel TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IObservableCollection layerPropertyGroups); + TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups); +} - public interface IConditionVmFactory : IVmFactory - { - StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); - EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); - } +public interface IConditionVmFactory : IVmFactory +{ + StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition); + EventConditionViewModel EventConditionViewModel(EventCondition eventCondition); +} - public interface IPrerequisitesVmFactory : IVmFactory - { - PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); - } +public interface IPrerequisitesVmFactory : IVmFactory +{ + PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); +} - public interface IScriptVmFactory : IVmFactory - { - ScriptsDialogViewModel ScriptsDialogViewModel(Profile profile); - ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); - } +public interface IScriptVmFactory : IVmFactory +{ + ScriptsDialogViewModel ScriptsDialogViewModel(Profile profile); + ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); +} - public interface ISidebarVmFactory : IVmFactory - { - SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); - SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); - ProfileConfigurationHotkeyViewModel ProfileConfigurationHotkeyViewModel(ProfileConfiguration profileConfiguration, bool isDisableHotkey); - ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement); - } +public interface ISidebarVmFactory : IVmFactory +{ + SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); + SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); + ProfileConfigurationHotkeyViewModel ProfileConfigurationHotkeyViewModel(ProfileConfiguration profileConfiguration, bool isDisableHotkey); + ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement); +} - public interface INodeVmFactory : IVmFactory - { - NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript); - } +public interface INodeVmFactory : IVmFactory +{ + NodeScriptWindowViewModel NodeScriptWindowViewModel(NodeScript nodeScript); +} - public interface IPropertyVmFactory - { - ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); - ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); - } +public interface IPropertyVmFactory +{ + ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); + ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel); } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs b/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs new file mode 100644 index 000000000..118de3d70 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs @@ -0,0 +1,20 @@ +using Avalonia.Media; +using SkiaSharp; + +namespace Artemis.UI.Shared.Extensions; + +/// +/// Provides extension methods for the type. +/// +public static class SKColorExtensions +{ + /// + /// Converts a SkiaSharp to an Avalonia . + /// + /// The color to convert. + /// The resulting color. + public static Color ToColor(this SKColor color) + { + return new Color(color.Alpha, color.Red, color.Green, color.Blue); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs index 0df32684d..33aabb2c4 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/MoveNode.cs @@ -29,6 +29,24 @@ public class MoveNode : INodeEditorCommand _originalY = node.Y; } + /// + /// Creates a new instance of the class. + /// + /// The node to update. + /// The new X-position. + /// The new Y-position. + /// The original X-position. + /// The original Y-position. + public MoveNode(INode node, double x, double y, double originalX, double originalY) + { + _node = node; + _x = x; + _y = y; + + _originalX = originalX; + _originalY = originalY; + } + /// public string DisplayName => "Move node"; diff --git a/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs b/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs index 7bf4a26e7..be37a5646 100644 --- a/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs +++ b/src/Avalonia/Artemis.UI/Converters/ColorToSolidColorBrushConverter.cs @@ -2,29 +2,24 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; -using RGBColor = RGB.NET.Core.Color; namespace Artemis.UI.Converters { /// - /// Converts into . + /// Converts into . /// public class ColorToSolidColorBrushConverter : IValueConverter { /// public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - return new SolidColorBrush(!(value is RGBColor color) - ? new Color(0, 0, 0, 0) - : new Color((byte) color.A, (byte) color.R, (byte) color.G, (byte) color.B)); + return new SolidColorBrush(value is not Color color ? new Color(0, 0, 0, 0) : color); } /// public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { - return !(value is SolidColorBrush brush) - ? RGBColor.Transparent - : new RGBColor(brush.Color.A, brush.Color.R, brush.Color.G, brush.Color.B); + return value is not SolidColorBrush brush ? new Color(0, 0, 0, 0) : brush.Color; } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 140914580..fb9493c2c 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -95,13 +95,14 @@ namespace Artemis.UI.Ninject.Factories NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript); NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node); + CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to); } public interface INodePinVmFactory { - PinCollectionViewModel InputPinCollectionViewModel(PinCollection inputPinCollection); + PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection); PinViewModel InputPinViewModel(IPin inputPin); - PinCollectionViewModel OutputPinCollectionViewModel(PinCollection outputPinCollection); + PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection); PinViewModel OutputPinViewModel(IPin outputPin); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml index 12b214fb4..01c50c703 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml @@ -2,7 +2,38 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" + xmlns:converters="clr-namespace:Artemis.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.CableView"> - Welcome to Avalonia! - + x:Class="Artemis.UI.Screens.VisualScripting.CableView" + x:DataType="visualScripting:CableViewModel" + ClipToBounds="False"> + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs index b4fa8c6a4..1c16f8cf0 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -1,20 +1,30 @@ +using System; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Shapes; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; +using ReactiveUI; -namespace Artemis.UI.Screens.VisualScripting +namespace Artemis.UI.Screens.VisualScripting; + +public class CableView : ReactiveUserControl { - public partial class CableView : ReactiveUserControl + public CableView() { - public CableView() - { - InitializeComponent(); - } + InitializeComponent(); + Path cablePath = this.Get("CablePath"); - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } + // Swap a margin on and off of the cable path to ensure the visual is always invalidated + // This is a workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748 + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.FromPoint) + .Subscribe(_ => cablePath.Margin = cablePath.Margin == new Thickness(0, 0, 0, 0) ? new Thickness(1, 1, 0, 0) : new Thickness(0, 0, 0, 0)) + .DisposeWith(d)); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs index 9d14d845d..06f202258 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -1,28 +1,128 @@ -using Artemis.UI.Screens.VisualScripting.Pins; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; +using Avalonia; +using Avalonia.Media; +using Avalonia.Threading; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { - private PinViewModel _from; - private PinViewModel _to; + private const double CABLE_OFFSET = 24 * 4; - public CableViewModel(PinViewModel from, PinViewModel to) + private readonly NodeScriptViewModel _nodeScriptViewModel; + private PinDirection _dragDirection; + private Point _dragPoint; + private bool _isDragging; + private IPin? _from; + private IPin? _to; + private PinViewModel? _fromViewModel; + private PinViewModel? _toViewModel; + private readonly ObservableAsPropertyHelper _fromPoint; + private readonly ObservableAsPropertyHelper _fromTargetPoint; + private readonly ObservableAsPropertyHelper _toPoint; + private readonly ObservableAsPropertyHelper _toTargetPoint; + private readonly ObservableAsPropertyHelper _cableColor; + + public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to) { + if (from != null && from.Direction != PinDirection.Output) + throw new ArtemisUIException("Can only create cables originating from an output pin"); + if (to != null && to.Direction != PinDirection.Input) + throw new ArtemisUIException("Can only create cables targeted to an input pin"); + + _nodeScriptViewModel = nodeScriptViewModel; _from = from; _to = to; + + this.WhenActivated(d => + { + if (From != null) + _nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == From).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d); + if (To != null) + _nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == To).Transform(model => ToViewModel = model).Subscribe().DisposeWith(d); + }); + + _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()).Switch().ToProperty(this, vm => vm.FromPoint); + _fromTargetPoint = this.WhenAnyValue(vm => vm.FromPoint).Select(point => new Point(point.X + CABLE_OFFSET, point.Y)).ToProperty(this, vm => vm.FromTargetPoint); + _toPoint = this.WhenAnyValue(vm => vm.ToViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()).Switch().ToProperty(this, vm => vm.ToPoint); + _toTargetPoint = this.WhenAnyValue(vm => vm.ToPoint).Select(point => new Point(point.X - CABLE_OFFSET, point.Y)).ToProperty(this, vm => vm.ToTargetPoint); + _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel).Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)).ToProperty(this, vm => vm.CableColor); } - public PinViewModel From + public IPin? From { get => _from; set => RaiseAndSetIfChanged(ref _from, value); } - public PinViewModel To + public IPin? To { get => _to; set => RaiseAndSetIfChanged(ref _to, value); } + + public PinViewModel? FromViewModel + { + get => _fromViewModel; + set => RaiseAndSetIfChanged(ref _fromViewModel, value); + } + + public PinViewModel? ToViewModel + { + get => _toViewModel; + set => RaiseAndSetIfChanged(ref _toViewModel, value); + } + + public bool IsDragging + { + get => _isDragging; + set => RaiseAndSetIfChanged(ref _isDragging, value); + } + + public PinDirection DragDirection + { + get => _dragDirection; + set => RaiseAndSetIfChanged(ref _dragDirection, value); + } + + public Point DragPoint + { + get => _dragPoint; + set => RaiseAndSetIfChanged(ref _dragPoint, value); + } + + public Point FromPoint => _fromPoint.Value; + public Point FromTargetPoint => _fromTargetPoint.Value; + public Point ToPoint => _toPoint.Value; + public Point ToTargetPoint => _toTargetPoint.Value; + public Color CableColor => _cableColor.Value; + + public void StartDrag(PinDirection dragDirection) + { + IsDragging = true; + DragDirection = dragDirection; + } + + public bool UpdateDrag(Point position, PinViewModel? targetViewModel) + { + DragPoint = position; + return true; + } + + public void FinishDrag() + { + IsDragging = false; + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml index b08cf9b2c..100b3d59c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml @@ -19,7 +19,6 @@ - + @@ -41,10 +41,10 @@ - + - + @@ -56,7 +56,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 26f42fd97..62c787d7d 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -6,10 +6,14 @@ using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Events; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; +using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -17,11 +21,13 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptViewModel : ActivatableViewModelBase { private readonly INodeVmFactory _nodeVmFactory; + private readonly INodeEditorService _nodeEditorService; private List? _initialNodeSelection; public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) { _nodeVmFactory = nodeVmFactory; + _nodeEditorService = nodeEditorService; NodeScript = nodeScript; NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); @@ -37,16 +43,46 @@ public class NodeScriptViewModel : ActivatableViewModelBase .DisposeWith(d); }); + // Create VMs for all nodes NodeViewModels = new ObservableCollection(); foreach (INode nodeScriptNode in NodeScript.Nodes) NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); - CableViewModels = new ObservableCollection(); + // Observe all outgoing pin connections and create cables for them + IObservable> viewModels = NodeViewModels.ToObservableChangeSet(); + PinViewModels = viewModels.TransformMany(vm => vm.OutputPinViewModels) + .Merge(viewModels.TransformMany(vm => vm.InputPinViewModels)) + .Merge(viewModels + .TransformMany(vm => vm.OutputPinCollectionViewModels) + .TransformMany(vm => vm.PinViewModels)) + .Merge(viewModels + .TransformMany(vm => vm.InputPinCollectionViewModels) + .TransformMany(vm => vm.PinViewModels)) + .AsObservableList(); + + PinViewModels.Connect() + .Filter(p => p.Pin.Direction == PinDirection.Input && p.Pin.ConnectedTo.Any()) + .Transform(vm => _nodeVmFactory.CableViewModel(this, vm.Pin.ConnectedTo.First(), vm.Pin)) // The first pin is the originating output pin + .Bind(out ReadOnlyObservableCollection cableViewModels) + .Subscribe(); + + CableViewModels = cableViewModels; + } + + public IObservableList PinViewModels { get; } + + public PinViewModel? GetPinViewModel(IPin pin) + { + return NodeViewModels + .SelectMany(n => n.Pins) + .Concat(NodeViewModels.SelectMany(n => n.InputPinCollectionViewModels.SelectMany(c => c.PinViewModels))) + .Concat(NodeViewModels.SelectMany(n => n.OutputPinCollectionViewModels.SelectMany(c => c.PinViewModels))) + .FirstOrDefault(vm => vm.Pin == pin); } public NodeScript NodeScript { get; } public ObservableCollection NodeViewModels { get; } - public ObservableCollection CableViewModels { get; } + public ReadOnlyObservableCollection CableViewModels { get; } public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } @@ -87,18 +123,28 @@ public class NodeScriptViewModel : ActivatableViewModelBase public void StartNodeDrag(Point position) { foreach (NodeViewModel nodeViewModel in NodeViewModels) - nodeViewModel.SaveDragOffset(position); + nodeViewModel.StartDrag(position); } public void UpdateNodeDrag(Point position) { foreach (NodeViewModel nodeViewModel in NodeViewModels) - nodeViewModel.UpdatePosition(position); + nodeViewModel.UpdateDrag(position); } public void FinishNodeDrag() { - // TODO: Command + List commands = NodeViewModels.Select(n => n.FinishDrag()).Where(c => c != null).Cast().ToList(); + + if (!commands.Any()) + return; + + if (commands.Count == 1) + _nodeEditorService.ExecuteCommand(NodeScript, commands.First()); + + using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, $"Move {commands.Count} nodes"); + foreach (MoveNode moveNode in commands) + _nodeEditorService.ExecuteCommand(NodeScript, moveNode); } private void HandleNodeAdded(SingleValueEventArgs eventArgs) diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index 33b2bb1b2..37aa77d57 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -57,14 +57,14 @@ - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index 6a3cf384a..be0b892dc 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; using Avalonia.Input; using Avalonia.Markup.Xaml; @@ -50,27 +51,36 @@ public class NodeView : ReactiveUserControl _dragging = false; ViewModel.NodeScriptViewModel.FinishNodeDrag(); e.Pointer.Capture(null); - return; } - - ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); - ViewModel.NodeScriptViewModel.FinishNodeSelection(); + else + { + ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + ViewModel.NodeScriptViewModel.FinishNodeSelection(); + } e.Handled = true; } private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e) { - PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType()); + PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType()); if (ViewModel == null || !point.Properties.IsLeftButtonPressed) return; if (!_dragging) { _dragging = true; + + if (!ViewModel.IsSelected) + { + ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List {ViewModel}, false, false); + ViewModel.NodeScriptViewModel.FinishNodeSelection(); + } + ViewModel.NodeScriptViewModel.StartNodeDrag(point.Position); e.Pointer.Capture((IInputElement?) sender); } + ViewModel.NodeScriptViewModel.UpdateNodeDrag(point.Position); e.Handled = true; diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 13eb8c500..1f25b3cd1 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -4,7 +4,6 @@ using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Ninject.Factories; -using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; @@ -12,6 +11,7 @@ using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; using Avalonia.Controls.Mixins; using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -22,10 +22,12 @@ public class NodeViewModel : ActivatableViewModelBase private ICustomNodeViewModel? _customNodeViewModel; private ReactiveCommand? _deleteNode; - private ObservableAsPropertyHelper? _isStaticNode; private double _dragOffsetX; private double _dragOffsetY; private bool _isSelected; + private ObservableAsPropertyHelper? _isStaticNode; + private double _startX; + private double _startY; public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { @@ -33,7 +35,39 @@ public class NodeViewModel : ActivatableViewModelBase _nodeEditorService = nodeEditorService; Node = node; + DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); + SourceList nodePins = new(); + SourceList nodePinCollections = new(); + nodePins.AddRange(Node.Pins); + nodePinCollections.AddRange(Node.PinCollections); + + // Create observable collections split up by direction + nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel) + .Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); + nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel) + .Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); + InputPinViewModels = inputPins; + OutputPinViewModels = outputPins; + + // Same again but for pin collections + nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinCollectionViewModel) + .Bind(out ReadOnlyObservableCollection inputPinCollections).Subscribe(); + nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinCollectionViewModel) + .Bind(out ReadOnlyObservableCollection outputPinCollections).Subscribe(); + InputPinCollectionViewModels = inputPinCollections; + OutputPinCollectionViewModels = outputPinCollections; + + // Create a single observable collection containing all pin view models + InputPinViewModels.ToObservableChangeSet() + .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Merge(OutputPinViewModels.ToObservableChangeSet()) + .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Bind(out ReadOnlyObservableCollection pins) + .Subscribe(); + + Pins = pins; + this.WhenActivated(d => { _isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode) @@ -41,29 +75,30 @@ public class NodeViewModel : ActivatableViewModelBase .ToProperty(this, model => model.IsStaticNode) .DisposeWith(d); - Node.WhenAnyValue(n => n.Pins).Subscribe(pins => nodePins.Edit(source => + // Subscribe to pin changes + Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source => { source.Clear(); - source.AddRange(pins); + source.AddRange(p); + })).DisposeWith(d); + // Subscribe to pin collection changes + Node.WhenAnyValue(n => n.PinCollections).Subscribe(c => nodePinCollections.Edit(source => + { + source.Clear(); + source.AddRange(c); })).DisposeWith(d); }); - - DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v)); - - nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel).Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); - nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel).Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); - InputPinViewModels = inputPins; - OutputPinViewModels = outputPins; } public bool IsStaticNode => _isStaticNode?.Value ?? true; - public NodeScriptViewModel NodeScriptViewModel { get; set; } + public NodeScriptViewModel NodeScriptViewModel { get; } public INode Node { get; } public ReadOnlyObservableCollection InputPinViewModels { get; } public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } public ReadOnlyObservableCollection OutputPinViewModels { get; } public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; } + public ReadOnlyObservableCollection Pins { get; } public ICustomNodeViewModel? CustomNodeViewModel { @@ -83,16 +118,18 @@ public class NodeViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _isSelected, value); } - public void SaveDragOffset(Point mouseStartPosition) + public void StartDrag(Point mouseStartPosition) { if (!IsSelected) return; _dragOffsetX = Node.X - mouseStartPosition.X; _dragOffsetY = Node.Y - mouseStartPosition.Y; + _startX = Node.X; + _startY = Node.Y; } - public void UpdatePosition(Point mousePosition) + public void UpdateDrag(Point mousePosition) { if (!IsSelected) return; @@ -101,6 +138,13 @@ public class NodeViewModel : ActivatableViewModelBase Node.Y = Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10d; } + public MoveNode? FinishDrag() + { + if (IsSelected && (Math.Abs(_startX - Node.X) > 0.01 || Math.Abs(_startY - Node.Y) > 0.01)) + return new MoveNode(Node, Node.X, Node.Y, _startX, _startY); + return null; + } + private void ExecuteDeleteNode() { _nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node)); diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml index 3bf8ae477..a6a0f7e98 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml @@ -2,7 +2,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView"> - Welcome to Avalonia! + x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView" + x:DataType="pins:PinCollectionViewModel"> + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs index 75488b75e..0edea5c81 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.UI.Ninject.Factories; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -6,7 +7,7 @@ public class InputPinCollectionViewModel : PinCollectionViewModel { public InputPinCollection InputPinCollection { get; } - public InputPinCollectionViewModel(InputPinCollection inputPinCollection) : base(inputPinCollection) + public InputPinCollectionViewModel(InputPinCollection inputPinCollection, INodePinVmFactory nodePinVmFactory) : base(inputPinCollection, nodePinVmFactory) { InputPinCollection = inputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml index 523457358..320a42a0c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs index 44a65692c..8a2c267e5 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs @@ -1,20 +1,86 @@ +using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; +using Avalonia.Input; +using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; namespace Artemis.UI.Screens.VisualScripting.Pins { public partial class InputPinView : ReactiveUserControl { + private bool _dragging; + private readonly Border _pinPoint; + private Canvas? _container; + public InputPinView() { InitializeComponent(); + _pinPoint = this.Get("PinPoint"); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void PinPoint_OnPointerMoved(object? sender, PointerEventArgs e) + { + ZoomBorder? zoomBorder = this.FindAncestorOfType(); + PointerPoint point = e.GetCurrentPoint(zoomBorder); + if (ViewModel == null || zoomBorder == null || !point.Properties.IsLeftButtonPressed) + return; + + if (!_dragging) + { + e.Pointer.Capture(_pinPoint); + // ViewModel.StartDrag(); + } + + PointerPoint absolutePosition = e.GetCurrentPoint(null); + OutputPinView? target = (OutputPinView?) zoomBorder.GetLogicalDescendants().FirstOrDefault(d => d is OutputPinView v && v.TransformedBounds != null && v.TransformedBounds.Value.Contains(absolutePosition.Position)); + + // ViewModel.UpdateDrag(point.Position, target?.ViewModel); + e.Handled = true; + } + + private void PinPoint_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_dragging) + return; + + _dragging = false; + e.Pointer.Capture(null); + // ViewModel.FinishDrag(); + e.Handled = true; + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _container = this.FindAncestorOfType(); + } + + /// + public override void Render(DrawingContext context) + { + base.Render(context); + UpdatePosition(); + } + + private void UpdatePosition() + { + if (_container == null || ViewModel == null) + return; + + Matrix? transform = this.TransformToVisual(_container); + if (transform != null) + ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + } } } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml index 424369776..091246cd8 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml @@ -2,7 +2,18 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView"> - Welcome to Avalonia! + x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView" + x:DataType="pins:PinCollectionViewModel"> + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs index 7d8400019..c268a3bfd 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs @@ -1,4 +1,5 @@ using Artemis.Core; +using Artemis.UI.Ninject.Factories; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -6,7 +7,7 @@ public class OutputPinCollectionViewModel : PinCollectionViewModel { public OutputPinCollection OutputPinCollection { get; } - public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection) : base(outputPinCollection) + public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection, INodePinVmFactory nodePinVmFactory) : base(outputPinCollection, nodePinVmFactory) { OutputPinCollection = outputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs index 175d9bd0d..286dc7fcf 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs @@ -1,20 +1,50 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.PanAndZoom; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting.Pins { public partial class OutputPinView : ReactiveUserControl { + private Canvas? _container; + public OutputPinView() { InitializeComponent(); } + /// + public override void Render(DrawingContext context) + { + base.Render(context); + UpdatePosition(); + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _container = this.FindAncestorOfType(); + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void UpdatePosition() + { + if (_container == null || ViewModel == null) + return; + + Matrix? transform = this.TransformToVisual(_container); + if (transform != null) + ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs index 42140ec4f..e521ecc8e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs @@ -1,14 +1,51 @@ -using Artemis.Core; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Events; +using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; +using Avalonia.Controls.Mixins; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinCollectionViewModel : ActivatableViewModelBase { - public PinCollection PinCollection { get; } + private readonly INodePinVmFactory _nodePinVmFactory; + public IPinCollection PinCollection { get; } + public ObservableCollection PinViewModels { get; } - protected PinCollectionViewModel(PinCollection pinCollection) + protected PinCollectionViewModel(IPinCollection pinCollection, INodePinVmFactory nodePinVmFactory) { + _nodePinVmFactory = nodePinVmFactory; + PinCollection = pinCollection; + PinViewModels = new ObservableCollection(); + + this.WhenActivated(d => + { + PinViewModels.Clear(); + PinViewModels.AddRange(PinCollection.Select(CreatePinViewModel)); + + Observable.FromEventPattern>(x => PinCollection.PinAdded += x, x => PinCollection.PinAdded -= x) + .Subscribe(e => PinViewModels.Add(CreatePinViewModel(e.EventArgs.Value))) + .DisposeWith(d); + Observable.FromEventPattern>(x => PinCollection.PinRemoved += x, x => PinCollection.PinRemoved -= x) + .Subscribe(e => PinViewModels.RemoveMany(PinViewModels.Where(p => p.Pin == e.EventArgs.Value).ToList())) + .DisposeWith(d); + }); + + AddPin = ReactiveCommand.Create(() => PinCollection.AddPin()); + } + + public ReactiveCommand AddPin { get; } + + private PinViewModel CreatePinViewModel(IPin pin) + { + return PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs index 0b1ce6202..84d9b1430 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -1,22 +1,62 @@ -using Artemis.Core; +using System; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Events; using Artemis.Core.Services; using Artemis.UI.Shared; +using Artemis.UI.Shared.Extensions; +using Avalonia; +using Avalonia.Controls.Mixins; using Avalonia.Media; +using DynamicData; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinViewModel : ActivatableViewModelBase { + private Point _position; + protected PinViewModel(IPin pin, INodeService nodeService) { Pin = pin; TypeColorRegistration registration = nodeService.GetTypeColorRegistration(Pin.Type); - PinColor = new Color(registration.Color.Alpha, registration.Color.Red, registration.Color.Green, registration.Color.Blue); - DarkenedPinColor = new Color(registration.DarkenedColor.Alpha, registration.DarkenedColor.Red, registration.DarkenedColor.Green, registration.DarkenedColor.Blue); + PinColor = registration.Color.ToColor(); + DarkenedPinColor = registration.DarkenedColor.ToColor(); + + SourceList connectedPins = new(); + this.WhenActivated(d => + { + Observable.FromEventPattern>(x => Pin.PinConnected += x, x => Pin.PinConnected -= x) + .Subscribe(e => connectedPins.Add(e.EventArgs.Value)) + .DisposeWith(d); + Observable.FromEventPattern>(x => Pin.PinDisconnected += x, x => Pin.PinDisconnected -= x) + .Subscribe(e => connectedPins.Remove(e.EventArgs.Value)) + .DisposeWith(d); + }); + + Connections = connectedPins.Connect().AsObservableList(); + connectedPins.AddRange(Pin.ConnectedTo); } + public IObservableList Connections { get; } + public IPin Pin { get; } public Color PinColor { get; } public Color DarkenedPinColor { get; } + + public Point Position + { + get => _position; + set => RaiseAndSetIfChanged(ref _position, value); + } + + public bool IsTypeCompatible(Type type) + { + return Pin.Type == type + || Pin.Type == typeof(Enum) && type.IsEnum + || Pin.Direction == PinDirection.Input && Pin.Type == typeof(object) + || Pin.Direction == PinDirection.Output && type == typeof(object); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml index afd62ecf2..9dbad6ef5 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/VisualScripting.axaml @@ -8,6 +8,7 @@ - + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 44930e0ee..16018ec21 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -63,6 +63,9 @@ LayerShapeVisualizerView.axaml + + DragCableView.axaml + diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index fb9493c2c..4bada05d2 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -95,7 +95,8 @@ namespace Artemis.UI.Ninject.Factories NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript); NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node); - CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to); + CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to); + DragCableViewModel DragCableViewModel(PinViewModel pinViewModel); } public interface INodePinVmFactory diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml index 01c50c703..c60dda5ff 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml @@ -11,29 +11,25 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs index 1c16f8cf0..c8f92e883 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -1,9 +1,11 @@ using System; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Mixins; using Avalonia.Controls.Shapes; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.ReactiveUI; using ReactiveUI; @@ -11,20 +13,38 @@ namespace Artemis.UI.Screens.VisualScripting; public class CableView : ReactiveUserControl { + private const double CABLE_OFFSET = 24 * 4; + private readonly Path _cablePath; + public CableView() { InitializeComponent(); - Path cablePath = this.Get("CablePath"); + _cablePath = this.Get("CablePath"); - // Swap a margin on and off of the cable path to ensure the visual is always invalidated - // This is a workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748 - this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.FromPoint) - .Subscribe(_ => cablePath.Margin = cablePath.Margin == new Thickness(0, 0, 0, 0) ? new Thickness(1, 1, 0, 0) : new Thickness(0, 0, 0, 0)) - .DisposeWith(d)); + // Not using bindings here to avoid a warnings + this.WhenActivated(d => + { + ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d); + ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } + + private void Update() + { + // Workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748 + _cablePath.Margin = _cablePath.Margin != new Thickness(0, 0, 0, 0) ? new Thickness(0, 0, 0, 0) : new Thickness(1, 1, 0, 0); + + PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First(); + BezierSegment segment = (BezierSegment) pathFigure.Segments!.First(); + pathFigure.StartPoint = ViewModel!.FromPoint; + segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y); + segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y); + segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs index 06f202258..38991d633 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -1,76 +1,53 @@ using System; -using System.ComponentModel; -using System.Diagnostics; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Exceptions; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; -using Artemis.UI.Shared.Extensions; using Avalonia; using Avalonia.Media; -using Avalonia.Threading; using DynamicData; +using DynamicData.Binding; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { - private const double CABLE_OFFSET = 24 * 4; + private readonly ObservableAsPropertyHelper _cableColor; + private readonly ObservableAsPropertyHelper _fromPoint; + private readonly ObservableAsPropertyHelper _toPoint; - private readonly NodeScriptViewModel _nodeScriptViewModel; - private PinDirection _dragDirection; - private Point _dragPoint; - private bool _isDragging; - private IPin? _from; - private IPin? _to; private PinViewModel? _fromViewModel; private PinViewModel? _toViewModel; - private readonly ObservableAsPropertyHelper _fromPoint; - private readonly ObservableAsPropertyHelper _fromTargetPoint; - private readonly ObservableAsPropertyHelper _toPoint; - private readonly ObservableAsPropertyHelper _toTargetPoint; - private readonly ObservableAsPropertyHelper _cableColor; - public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin? from, IPin? to) + public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) { - if (from != null && from.Direction != PinDirection.Output) + if (from.Direction != PinDirection.Output) throw new ArtemisUIException("Can only create cables originating from an output pin"); - if (to != null && to.Direction != PinDirection.Input) + if (to.Direction != PinDirection.Input) throw new ArtemisUIException("Can only create cables targeted to an input pin"); - _nodeScriptViewModel = nodeScriptViewModel; - _from = from; - _to = to; this.WhenActivated(d => { - if (From != null) - _nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == From).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d); - if (To != null) - _nodeScriptViewModel.PinViewModels.Connect().Filter(p => p.Pin == To).Transform(model => ToViewModel = model).Subscribe().DisposeWith(d); + nodeScriptViewModel.PinViewModels.ToObservableChangeSet().Filter(p => ReferenceEquals(p.Pin, from)).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d); + nodeScriptViewModel.PinViewModels.ToObservableChangeSet().Filter(p => ReferenceEquals(p.Pin, to)).Transform(model => ToViewModel = model).Subscribe().DisposeWith(d); }); - _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()).Switch().ToProperty(this, vm => vm.FromPoint); - _fromTargetPoint = this.WhenAnyValue(vm => vm.FromPoint).Select(point => new Point(point.X + CABLE_OFFSET, point.Y)).ToProperty(this, vm => vm.FromTargetPoint); - _toPoint = this.WhenAnyValue(vm => vm.ToViewModel).Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()).Switch().ToProperty(this, vm => vm.ToPoint); - _toTargetPoint = this.WhenAnyValue(vm => vm.ToPoint).Select(point => new Point(point.X - CABLE_OFFSET, point.Y)).ToProperty(this, vm => vm.ToTargetPoint); - _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel).Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)).ToProperty(this, vm => vm.CableColor); - } + _fromPoint = this.WhenAnyValue(vm => vm.FromViewModel) + .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.FromPoint); + _toPoint = this.WhenAnyValue(vm => vm.ToViewModel) + .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.ToPoint); - public IPin? From - { - get => _from; - set => RaiseAndSetIfChanged(ref _from, value); - } - - public IPin? To - { - get => _to; - set => RaiseAndSetIfChanged(ref _to, value); + _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel) + .Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)) + .ToProperty(this, vm => vm.CableColor); } public PinViewModel? FromViewModel @@ -85,44 +62,7 @@ public class CableViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _toViewModel, value); } - public bool IsDragging - { - get => _isDragging; - set => RaiseAndSetIfChanged(ref _isDragging, value); - } - - public PinDirection DragDirection - { - get => _dragDirection; - set => RaiseAndSetIfChanged(ref _dragDirection, value); - } - - public Point DragPoint - { - get => _dragPoint; - set => RaiseAndSetIfChanged(ref _dragPoint, value); - } - public Point FromPoint => _fromPoint.Value; - public Point FromTargetPoint => _fromTargetPoint.Value; public Point ToPoint => _toPoint.Value; - public Point ToTargetPoint => _toTargetPoint.Value; public Color CableColor => _cableColor.Value; - - public void StartDrag(PinDirection dragDirection) - { - IsDragging = true; - DragDirection = dragDirection; - } - - public bool UpdateDrag(Point position, PinViewModel? targetViewModel) - { - DragPoint = position; - return true; - } - - public void FinishDrag() - { - IsDragging = false; - } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml new file mode 100644 index 000000000..579726b33 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs new file mode 100644 index 000000000..243576bb9 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableView.axaml.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Shapes; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.VisualScripting; + +public class DragCableView : ReactiveUserControl +{ + private const double CABLE_OFFSET = 24 * 4; + private readonly Path _cablePath; + + public DragCableView() + { + InitializeComponent(); + _cablePath = this.Get("CablePath"); + + // Not using bindings here to avoid warnings + this.WhenActivated(d => + { + ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d); + ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); + Update(); + }); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Update() + { + PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First(); + BezierSegment segment = (BezierSegment) pathFigure.Segments!.First(); + pathFigure.StartPoint = ViewModel!.FromPoint; + segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y); + segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y); + segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs new file mode 100644 index 000000000..39bb9259a --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/DragCableViewModel.cs @@ -0,0 +1,51 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.UI.Screens.VisualScripting.Pins; +using Artemis.UI.Shared; +using Avalonia; +using ReactiveUI; + +namespace Artemis.UI.Screens.VisualScripting; + +public class DragCableViewModel : ActivatableViewModelBase +{ + private Point _dragPoint; + + private ObservableAsPropertyHelper? _fromPoint; + private ObservableAsPropertyHelper? _toPoint; + + public DragCableViewModel(PinViewModel pinViewModel) + { + PinViewModel = pinViewModel; + + // If the pin is output, the pin is the from-point and the drag position is the to-point + if (PinViewModel.Pin.Direction == PinDirection.Output) + { + this.WhenActivated(d => + { + _fromPoint = PinViewModel.WhenAnyValue(vm => vm.Position).ToProperty(this, vm => vm.FromPoint).DisposeWith(d); + }); + _toPoint = this.WhenAnyValue(vm => vm.DragPoint).ToProperty(this, vm => vm.ToPoint); + } + // If the pin is input, the pin is the to-point and the drag position is the from-point; + else + { + this.WhenActivated(d => + { + _toPoint = PinViewModel.WhenAnyValue(vm => vm.Position).ToProperty(this, vm => vm.ToPoint).DisposeWith(d); + }); + _fromPoint = this.WhenAnyValue(vm => vm.DragPoint).ToProperty(this, vm => vm.FromPoint); + } + } + + public PinViewModel PinViewModel { get; } + public Point FromPoint => _fromPoint?.Value ?? new Point(0, 0); + public Point ToPoint => _toPoint?.Value ?? new Point(0, 0); + + public Point DragPoint + { + get => _dragPoint; + set => RaiseAndSetIfChanged(ref _dragPoint, value); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml index 100b3d59c..a2f6256f3 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml @@ -40,6 +40,9 @@ + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index 9ace2bbf8..cf4a8e15b 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Artemis.UI.Shared.Controls; @@ -12,76 +11,74 @@ using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; -using Avalonia.VisualTree; -namespace Artemis.UI.Screens.VisualScripting +namespace Artemis.UI.Screens.VisualScripting; + +public class NodeScriptView : ReactiveUserControl { - public partial class NodeScriptView : ReactiveUserControl + private readonly Grid _grid; + private readonly ItemsControl _nodesContainer; + private readonly SelectionRectangle _selectionRectangle; + private readonly ZoomBorder _zoomBorder; + + public NodeScriptView() { - private readonly ZoomBorder _zoomBorder; - private readonly Grid _grid; - private readonly ItemsControl _nodesContainer; - private readonly SelectionRectangle _selectionRectangle; + InitializeComponent(); - public NodeScriptView() - { - InitializeComponent(); + _grid = this.Find("ContainerGrid"); + _zoomBorder = this.Find("ZoomBorder"); + _nodesContainer = this.Find("NodesContainer"); + _selectionRectangle = this.Find("SelectionRectangle"); + _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + UpdateZoomBorderBackground(); - _nodesContainer = this.Find("NodesContainer"); - _zoomBorder = this.Find("ZoomBorder"); - _grid = this.Find("ContainerGrid"); - _selectionRectangle = this.Find("SelectionRectangle"); - _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; + _grid.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); + } + + private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + // If the flyout handled the click, update the position of the node picker + if (e.Handled && ViewModel != null) + ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid); + } + + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(_zoomBorder.Background)) UpdateZoomBorderBackground(); + } - _grid?.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); - } + private void UpdateZoomBorderBackground() + { + if (_zoomBorder.Background is VisualBrush visualBrush) + visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); + } - private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - // If the flyout handled the click, update the position of the node picker - if (e.Handled && ViewModel != null) - ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid); - } + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } - private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.Name == nameof(_zoomBorder.Background)) - UpdateZoomBorderBackground(); - } + private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) + { + UpdateZoomBorderBackground(); + } - private void UpdateZoomBorderBackground() - { - if (_zoomBorder.Background is VisualBrush visualBrush) - visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); - } + private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) + { + List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); + List nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList(); + ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); + } - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } + private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) + { + ViewModel?.FinishNodeSelection(); + } - private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) - { - UpdateZoomBorderBackground(); - } - - private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) - { - List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); - List nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList(); - ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control)); - } - - private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e) - { - ViewModel?.FinishNodeSelection(); - } - - private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (!_selectionRectangle.IsSelecting) - ViewModel?.ClearNodeSelection(); - } + private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_selectionRectangle.IsSelecting) + ViewModel?.ClearNodeSelection(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 62c787d7d..7f97cc8ca 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -20,14 +20,17 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptViewModel : ActivatableViewModelBase { - private readonly INodeVmFactory _nodeVmFactory; private readonly INodeEditorService _nodeEditorService; + private readonly SourceList _nodeViewModels; + private readonly INodeVmFactory _nodeVmFactory; + private DragCableViewModel? _dragViewModel; private List? _initialNodeSelection; public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) { _nodeVmFactory = nodeVmFactory; _nodeEditorService = nodeEditorService; + _nodeViewModels = new SourceList(); NodeScript = nodeScript; NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); @@ -44,48 +47,42 @@ public class NodeScriptViewModel : ActivatableViewModelBase }); // Create VMs for all nodes - NodeViewModels = new ObservableCollection(); - foreach (INode nodeScriptNode in NodeScript.Nodes) - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); + _nodeViewModels.Connect().Bind(out ReadOnlyObservableCollection nodeViewModels).Subscribe(); + _nodeViewModels.Edit(l => + { + foreach (INode nodeScriptNode in NodeScript.Nodes) + l.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); + }); + NodeViewModels = nodeViewModels; + + NodeViewModels.ToObservableChangeSet().TransformMany(vm => vm.PinViewModels) + .Bind(out ReadOnlyObservableCollection pinViewModels) + .Subscribe(); + PinViewModels = pinViewModels; // Observe all outgoing pin connections and create cables for them - IObservable> viewModels = NodeViewModels.ToObservableChangeSet(); - PinViewModels = viewModels.TransformMany(vm => vm.OutputPinViewModels) - .Merge(viewModels.TransformMany(vm => vm.InputPinViewModels)) - .Merge(viewModels - .TransformMany(vm => vm.OutputPinCollectionViewModels) - .TransformMany(vm => vm.PinViewModels)) - .Merge(viewModels - .TransformMany(vm => vm.InputPinCollectionViewModels) - .TransformMany(vm => vm.PinViewModels)) - .AsObservableList(); - - PinViewModels.Connect() - .Filter(p => p.Pin.Direction == PinDirection.Input && p.Pin.ConnectedTo.Any()) - .Transform(vm => _nodeVmFactory.CableViewModel(this, vm.Pin.ConnectedTo.First(), vm.Pin)) // The first pin is the originating output pin + PinViewModels.ToObservableChangeSet() + .Filter(p => p.Pin.Direction == PinDirection.Output) + .TransformMany(p => p.Connections) + .Transform(pin => _nodeVmFactory.CableViewModel(this, pin.ConnectedTo.First(), pin)) .Bind(out ReadOnlyObservableCollection cableViewModels) .Subscribe(); - CableViewModels = cableViewModels; } - public IObservableList PinViewModels { get; } - - public PinViewModel? GetPinViewModel(IPin pin) - { - return NodeViewModels - .SelectMany(n => n.Pins) - .Concat(NodeViewModels.SelectMany(n => n.InputPinCollectionViewModels.SelectMany(c => c.PinViewModels))) - .Concat(NodeViewModels.SelectMany(n => n.OutputPinCollectionViewModels.SelectMany(c => c.PinViewModels))) - .FirstOrDefault(vm => vm.Pin == pin); - } - public NodeScript NodeScript { get; } - public ObservableCollection NodeViewModels { get; } + public ReadOnlyObservableCollection NodeViewModels { get; } + public ReadOnlyObservableCollection PinViewModels { get; } public ReadOnlyObservableCollection CableViewModels { get; } public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } + public DragCableViewModel? DragViewModel + { + get => _dragViewModel; + set => RaiseAndSetIfChanged(ref _dragViewModel, value); + } + public void UpdateNodeSelection(List nodes, bool expand, bool invert) { _initialNodeSelection ??= NodeViewModels.Where(vm => vm.IsSelected).ToList(); @@ -147,15 +144,37 @@ public class NodeScriptViewModel : ActivatableViewModelBase _nodeEditorService.ExecuteCommand(NodeScript, moveNode); } + public bool UpdatePinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position) + { + if (DragViewModel?.PinViewModel != sourcePinViewModel) + DragViewModel = new DragCableViewModel(sourcePinViewModel); + + DragViewModel.DragPoint = position; + + return targetPinVmModel == null || targetPinVmModel.IsCompatibleWith(sourcePinViewModel); + } + + public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel) + { + if (DragViewModel == null) + return; + + DragViewModel = null; + + // If dropped on top of a compatible pin, connect to it + if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel)) + _nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin)); + } + private void HandleNodeAdded(SingleValueEventArgs eventArgs) { - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); + _nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); } private void HandleNodeRemoved(SingleValueEventArgs eventArgs) { - NodeViewModel? toRemove = NodeViewModels.FirstOrDefault(vm => vm.Node == eventArgs.Value); + NodeViewModel? toRemove = NodeViewModels.FirstOrDefault(vm => ReferenceEquals(vm.Node, eventArgs.Value)); if (toRemove != null) - NodeViewModels.Remove(toRemove); + _nodeViewModels.Remove(toRemove); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 1f25b3cd1..144b76d5a 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -39,34 +39,44 @@ public class NodeViewModel : ActivatableViewModelBase SourceList nodePins = new(); SourceList nodePinCollections = new(); - nodePins.AddRange(Node.Pins); - nodePinCollections.AddRange(Node.PinCollections); // Create observable collections split up by direction - nodePins.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinViewModel) - .Bind(out ReadOnlyObservableCollection inputPins).Subscribe(); - nodePins.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinViewModel) - .Bind(out ReadOnlyObservableCollection outputPins).Subscribe(); + nodePins.Connect() + .Filter(n => n.Direction == PinDirection.Input) + .Transform(nodePinVmFactory.InputPinViewModel) + .Bind(out ReadOnlyObservableCollection inputPins) + .Subscribe(); + nodePins.Connect() + .Filter(n => n.Direction == PinDirection.Output) + .Transform(nodePinVmFactory.OutputPinViewModel) + .Bind(out ReadOnlyObservableCollection outputPins) + .Subscribe(); InputPinViewModels = inputPins; OutputPinViewModels = outputPins; // Same again but for pin collections - nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Input).Transform(nodePinVmFactory.InputPinCollectionViewModel) - .Bind(out ReadOnlyObservableCollection inputPinCollections).Subscribe(); - nodePinCollections.Connect().Filter(n => n.Direction == PinDirection.Output).Transform(nodePinVmFactory.OutputPinCollectionViewModel) - .Bind(out ReadOnlyObservableCollection outputPinCollections).Subscribe(); + nodePinCollections.Connect() + .Filter(n => n.Direction == PinDirection.Input) + .Transform(nodePinVmFactory.InputPinCollectionViewModel) + .Bind(out ReadOnlyObservableCollection inputPinCollections) + .Subscribe(); + nodePinCollections.Connect() + .Filter(n => n.Direction == PinDirection.Output) + .Transform(nodePinVmFactory.OutputPinCollectionViewModel) + .Bind(out ReadOnlyObservableCollection outputPinCollections) + .Subscribe(); InputPinCollectionViewModels = inputPinCollections; OutputPinCollectionViewModels = outputPinCollections; // Create a single observable collection containing all pin view models InputPinViewModels.ToObservableChangeSet() - .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Merge(OutputPinViewModels.ToObservableChangeSet()) + .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) .Bind(out ReadOnlyObservableCollection pins) .Subscribe(); - Pins = pins; + PinViewModels = pins; this.WhenActivated(d => { @@ -98,7 +108,7 @@ public class NodeViewModel : ActivatableViewModelBase public ReadOnlyObservableCollection InputPinCollectionViewModels { get; } public ReadOnlyObservableCollection OutputPinViewModels { get; } public ReadOnlyObservableCollection OutputPinCollectionViewModels { get; } - public ReadOnlyObservableCollection Pins { get; } + public ReadOnlyObservableCollection PinViewModels { get; } public ICustomNodeViewModel? CustomNodeViewModel { diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml index 320a42a0c..523457358 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml @@ -23,7 +23,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs index 8a2c267e5..817739ceb 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinView.axaml.cs @@ -1,86 +1,18 @@ -using System.Linq; -using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.PanAndZoom; -using Avalonia.Input; -using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; -using Avalonia.Media; -using Avalonia.ReactiveUI; -using Avalonia.VisualTree; -namespace Artemis.UI.Screens.VisualScripting.Pins +namespace Artemis.UI.Screens.VisualScripting.Pins; + +public class InputPinView : PinView { - public partial class InputPinView : ReactiveUserControl + public InputPinView() { - private bool _dragging; - private readonly Border _pinPoint; - private Canvas? _container; - - public InputPinView() - { - InitializeComponent(); - _pinPoint = this.Get("PinPoint"); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private void PinPoint_OnPointerMoved(object? sender, PointerEventArgs e) - { - ZoomBorder? zoomBorder = this.FindAncestorOfType(); - PointerPoint point = e.GetCurrentPoint(zoomBorder); - if (ViewModel == null || zoomBorder == null || !point.Properties.IsLeftButtonPressed) - return; - - if (!_dragging) - { - e.Pointer.Capture(_pinPoint); - // ViewModel.StartDrag(); - } - - PointerPoint absolutePosition = e.GetCurrentPoint(null); - OutputPinView? target = (OutputPinView?) zoomBorder.GetLogicalDescendants().FirstOrDefault(d => d is OutputPinView v && v.TransformedBounds != null && v.TransformedBounds.Value.Contains(absolutePosition.Position)); - - // ViewModel.UpdateDrag(point.Position, target?.ViewModel); - e.Handled = true; - } - - private void PinPoint_OnPointerReleased(object? sender, PointerReleasedEventArgs e) - { - if (!_dragging) - return; - - _dragging = false; - e.Pointer.Capture(null); - // ViewModel.FinishDrag(); - e.Handled = true; - } - - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - _container = this.FindAncestorOfType(); - } - - /// - public override void Render(DrawingContext context) - { - base.Render(context); - UpdatePosition(); - } - - private void UpdatePosition() - { - if (_container == null || ViewModel == null) - return; - - Matrix? transform = this.TransformToVisual(_container); - if (transform != null) - ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); - } + InitializeComponent(); + InitializePin(this.Get("PinPoint")); } -} + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs index 286dc7fcf..a62070e1d 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinView.axaml.cs @@ -1,50 +1,18 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.PanAndZoom; using Avalonia.Markup.Xaml; -using Avalonia.Media; -using Avalonia.ReactiveUI; -using Avalonia.VisualTree; -using ReactiveUI; -namespace Artemis.UI.Screens.VisualScripting.Pins +namespace Artemis.UI.Screens.VisualScripting.Pins; + +public class OutputPinView : PinView { - public partial class OutputPinView : ReactiveUserControl + public OutputPinView() { - private Canvas? _container; + InitializeComponent(); + InitializePin(this.Get("PinPoint")); + } - public OutputPinView() - { - InitializeComponent(); - } - - /// - public override void Render(DrawingContext context) - { - base.Render(context); - UpdatePosition(); - } - - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - _container = this.FindAncestorOfType(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private void UpdatePosition() - { - if (_container == null || ViewModel == null) - return; - - Matrix? transform = this.TransformToVisual(_container); - if (transform != null) - ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); - } + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs new file mode 100644 index 000000000..099936779 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs @@ -0,0 +1,90 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.ReactiveUI; +using Avalonia.VisualTree; + +namespace Artemis.UI.Screens.VisualScripting.Pins; + +public class PinView : ReactiveUserControl +{ + private bool _dragging; + private Canvas? _container; + private Border? _pinPoint; + + protected void InitializePin(Border pinPoint) + { + _pinPoint = pinPoint; + _pinPoint.PointerMoved += PinPointOnPointerMoved; + _pinPoint.PointerReleased += PinPointOnPointerReleased; + } + + private void PinPointOnPointerMoved(object? sender, PointerEventArgs e) + { + if (ViewModel == null || _container == null || _pinPoint == null) + return; + + NodeScriptViewModel? nodeScriptViewModel = this.FindAncestorOfType()?.ViewModel; + PointerPoint point = e.GetCurrentPoint(_container); + if (nodeScriptViewModel == null || !point.Properties.IsLeftButtonPressed) + return; + + if (!_dragging) + e.Pointer.Capture(_pinPoint); + + PinViewModel? targetPin = (_container.InputHitTest(point.Position) as Border)?.DataContext as PinViewModel; + if (targetPin == ViewModel) + targetPin = null; + + _pinPoint.Cursor = new Cursor(nodeScriptViewModel.UpdatePinDrag(ViewModel, targetPin, point.Position) ? StandardCursorType.Hand : StandardCursorType.No); + _dragging = true; + e.Handled = true; + } + + private void PinPointOnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (!_dragging || ViewModel == null || _container == null || _pinPoint == null) + return; + + _dragging = false; + e.Pointer.Capture(null); + + PointerPoint point = e.GetCurrentPoint(_container); + PinViewModel? targetPin = (_container.InputHitTest(point.Position) as Border)?.DataContext as PinViewModel; + if (targetPin == ViewModel) + targetPin = null; + + this.FindAncestorOfType()?.ViewModel?.FinishPinDrag(ViewModel, targetPin); + _pinPoint.Cursor = new Cursor(StandardCursorType.Hand); + e.Handled = true; + } + + #region Overrides of Visual + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _container = this.FindAncestorOfType(); + } + + /// + public override void Render(DrawingContext context) + { + base.Render(context); + UpdatePosition(); + } + + private void UpdatePosition() + { + if (_container == null || ViewModel == null) + return; + + Matrix? transform = this.TransformToVisual(_container); + if (transform != null) + ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs index 84d9b1430..ba6ad4d4e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -52,11 +52,16 @@ public abstract class PinViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _position, value); } - public bool IsTypeCompatible(Type type) + public bool IsCompatibleWith(PinViewModel pinViewModel) { - return Pin.Type == type - || Pin.Type == typeof(Enum) && type.IsEnum + if (pinViewModel.Pin.Direction == Pin.Direction) + return false; + if (pinViewModel.Pin.Node == Pin.Node) + return false; + + return Pin.Type == pinViewModel.Pin.Type + || Pin.Type == typeof(Enum) && pinViewModel.Pin.Type.IsEnum || Pin.Direction == PinDirection.Input && Pin.Type == typeof(object) - || Pin.Direction == PinDirection.Output && type == typeof(object); + || Pin.Direction == PinDirection.Output && pinViewModel.Pin.Type == typeof(object); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index eceeccf8b..930f8bdf0 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -40,8 +40,8 @@ namespace Artemis.UI.Screens.Workshop NodeScript testScript = new("Test script", "A test script"); INode exitNode = testScript.Nodes.Last(); - exitNode.X = 200; - exitNode.Y = 100; + exitNode.X = 300; + exitNode.Y = 150; OrNode orNode = new() {X = 100, Y = 100}; testScript.AddNode(orNode); From d9d237e0ebe27fbf51632ea8b4e2a7b662082fe9 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 20 Mar 2022 15:54:36 +0100 Subject: [PATCH 150/270] Node editor - Added node collection pin add/remove Node editor - Added first static value node --- src/Artemis.Core/Services/NodeService.cs | 4 + src/Artemis.Core/Utilities/Numeric.cs | 116 +++++++++++++++++- .../VisualScripting/InputPinCollection.cs | 13 +- .../Interfaces/IPinCollection.cs | 11 +- .../VisualScripting/NodeScript.cs | 2 +- .../VisualScripting/OutputPinCollection.cs | 10 +- .../VisualScripting/PinCollection.cs | 27 ++-- .../Wrapper/VisualScriptPinCollection.cs | 4 +- .../Controls/SelectionRectangle.cs | 10 +- .../Services/NodeEditor/Commands/AddNode.cs | 2 +- .../Services/NodeEditor/Commands/AddPin.cs | 38 ++++++ .../NodeEditor/Commands/ConnectPins.cs | 14 ++- .../NodeEditor/Commands/DeleteNode.cs | 16 ++- .../Services/NodeEditor/Commands/RemovePin.cs | 49 ++++++++ .../Artemis.UI.Shared/Styles/Button.axaml | 13 ++ .../Ninject/Factories/IVMFactory.cs | 4 +- .../Screens/VisualScripting/CableView.axaml | 90 ++++++++++---- .../VisualScripting/CableView.axaml.cs | 4 + .../Screens/VisualScripting/CableViewModel.cs | 16 ++- .../Screens/VisualScripting/NodeView.axaml | 62 +++++----- .../Screens/VisualScripting/NodeViewModel.cs | 25 +++- .../Pins/InputPinCollectionView.axaml | 17 ++- .../Pins/InputPinCollectionViewModel.cs | 4 +- .../Pins/OutputPinCollectionView.axaml | 16 ++- .../Pins/OutputPinCollectionViewModel.cs | 4 +- .../Pins/PinCollectionViewModel.cs | 19 ++- .../Screens/VisualScripting/Pins/PinView.cs | 6 +- .../VisualScripting/Pins/PinViewModel.cs | 8 ++ .../Screens/Workshop/WorkshopViewModel.cs | 30 +++-- .../Converters/NumericConverter.cs | 29 +++++ .../CustomViews/EnumEqualsNodeCustomView.xaml | 25 ---- .../LayerPropertyNodeCustomView.xaml | 17 --- .../StaticNumericValueNodeCustomView.axaml | 14 +++ .../StaticNumericValueNodeCustomView.axaml.cs | 18 +++ .../StaticNumericValueNodeCustomView.xaml | 15 --- .../StaticStringValueNodeCustomView.xaml | 11 -- 36 files changed, 575 insertions(+), 188 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs create mode 100644 src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs delete mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml delete mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs delete mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml delete mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index 92ee2d1f1..c0482363a 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -43,6 +43,10 @@ namespace Artemis.Core.Services if (match != null) return match; + // Objects represent an input that can take any type, these are hardcoded white + if (type == typeof(object)) + return new TypeColorRegistration(type, new SKColor(255, 255, 255, 255), Constants.CorePlugin); + // Come up with a random color based on the type name that should be the same each time MD5 md5Hasher = MD5.Create(); byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(type.FullName!)); diff --git a/src/Artemis.Core/Utilities/Numeric.cs b/src/Artemis.Core/Utilities/Numeric.cs index 86bec89b8..ac779d557 100644 --- a/src/Artemis.Core/Utilities/Numeric.cs +++ b/src/Artemis.Core/Utilities/Numeric.cs @@ -11,7 +11,7 @@ namespace Artemis.Core /// Usage outside that context is not recommended due to conversion overhead. /// /// - public readonly struct Numeric : IComparable + public readonly struct Numeric : IComparable, IConvertible { private readonly float _value; @@ -140,6 +140,11 @@ namespace Artemis.Core return p._value; } + public static implicit operator decimal(Numeric p) + { + return (decimal) p._value; + } + public static implicit operator byte(Numeric p) { return (byte) Math.Clamp(p._value, 0, 255); @@ -260,6 +265,112 @@ namespace Artemis.Core type == typeof(int) || type == typeof(byte); } + + #region Implementation of IConvertible + + /// + public TypeCode GetTypeCode() + { + return _value.GetTypeCode(); + } + + /// + public bool ToBoolean(IFormatProvider? provider) + { + return Convert.ToBoolean(_value); + } + + /// + public byte ToByte(IFormatProvider? provider) + { + return (byte) Math.Clamp(_value, 0, 255); + } + + /// + public char ToChar(IFormatProvider? provider) + { + return Convert.ToChar(_value); + } + + /// + public DateTime ToDateTime(IFormatProvider? provider) + { + return Convert.ToDateTime(_value); + } + + /// + public decimal ToDecimal(IFormatProvider? provider) + { + return (decimal) _value; + } + + /// + public double ToDouble(IFormatProvider? provider) + { + return _value; + } + + /// + public short ToInt16(IFormatProvider? provider) + { + return (short) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public int ToInt32(IFormatProvider? provider) + { + return (int) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public long ToInt64(IFormatProvider? provider) + { + return (long) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public sbyte ToSByte(IFormatProvider? provider) + { + return (sbyte) Math.Clamp(_value, 0, 255); + } + + /// + public float ToSingle(IFormatProvider? provider) + { + return _value; + } + + /// + public string ToString(IFormatProvider? provider) + { + return _value.ToString(provider); + } + + /// + public object ToType(Type conversionType, IFormatProvider? provider) + { + return Convert.ChangeType(_value, conversionType); + } + + /// + public ushort ToUInt16(IFormatProvider? provider) + { + return (ushort) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public uint ToUInt32(IFormatProvider? provider) + { + return (uint) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + /// + public ulong ToUInt64(IFormatProvider? provider) + { + return (ulong) MathF.Round(_value, MidpointRounding.AwayFromZero); + } + + #endregion } /// @@ -279,7 +390,8 @@ namespace Artemis.Core if (source == null) throw new ArgumentNullException(nameof(source)); float sum = 0; - foreach (float v in source) sum += v; + foreach (float v in source) + sum += v; return new Numeric(sum); } diff --git a/src/Artemis.Core/VisualScripting/InputPinCollection.cs b/src/Artemis.Core/VisualScripting/InputPinCollection.cs index 35fa7197f..fd4da2bf0 100644 --- a/src/Artemis.Core/VisualScripting/InputPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/InputPinCollection.cs @@ -14,8 +14,11 @@ namespace Artemis.Core #region Constructors internal InputPinCollection(INode node, string name, int initialCount) - : base(node, name, initialCount) + : base(node, name) { + // Can't do this in the base constructor because the type won't be set yet + for (int i = 0; i < initialCount; i++) + Add(CreatePin()); } #endregion @@ -23,7 +26,7 @@ namespace Artemis.Core #region Methods /// - protected override IPin CreatePin() + public override IPin CreatePin() { return new InputPin(Node, string.Empty); } @@ -59,13 +62,13 @@ namespace Artemis.Core #region Constructors internal InputPinCollection(INode node, Type type, string name, int initialCount) - : base(node, name, 0) + : base(node, name) { Type = type; // Can't do this in the base constructor because the type won't be set yet for (int i = 0; i < initialCount; i++) - AddPin(); + Add(CreatePin()); } #endregion @@ -73,7 +76,7 @@ namespace Artemis.Core #region Methods /// - protected override IPin CreatePin() + public override IPin CreatePin() { return new InputPin(Node, Type, string.Empty); } diff --git a/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs b/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs index bcbbf55e0..5bca752f7 100644 --- a/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/Interfaces/IPinCollection.cs @@ -40,10 +40,15 @@ namespace Artemis.Core event EventHandler> PinRemoved; /// - /// Creates a new pin and adds it to the collection + /// Creates a new pin compatible with this collection /// - /// The newly added pin - IPin AddPin(); + /// The newly created pin + IPin CreatePin(); + + /// + /// Adds the provided to the collection + /// + void Add(IPin pin); /// /// Removes the provided from the collection diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 263d995e8..f67476ceb 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -199,7 +199,7 @@ namespace Artemis.Core while (collection.Count() > entityNodePinCollection.Amount) collection.Remove(collection.Last()); while (collection.Count() < entityNodePinCollection.Amount) - collection.AddPin(); + collection.Add(collection.CreatePin()); } return node; diff --git a/src/Artemis.Core/VisualScripting/OutputPinCollection.cs b/src/Artemis.Core/VisualScripting/OutputPinCollection.cs index 465b37ae0..c479367d3 100644 --- a/src/Artemis.Core/VisualScripting/OutputPinCollection.cs +++ b/src/Artemis.Core/VisualScripting/OutputPinCollection.cs @@ -20,15 +20,19 @@ namespace Artemis.Core #region Constructors internal OutputPinCollection(INode node, string name, int initialCount) - : base(node, name, initialCount) - { } + : base(node, name) + { + // Can't do this in the base constructor because the type won't be set yet + for (int i = 0; i < initialCount; i++) + Add(CreatePin()); + } #endregion #region Methods /// - protected override IPin CreatePin() => new OutputPin(Node, string.Empty); + public override IPin CreatePin() => new OutputPin(Node, string.Empty); #endregion } diff --git a/src/Artemis.Core/VisualScripting/PinCollection.cs b/src/Artemis.Core/VisualScripting/PinCollection.cs index 04c781823..5c060d884 100644 --- a/src/Artemis.Core/VisualScripting/PinCollection.cs +++ b/src/Artemis.Core/VisualScripting/PinCollection.cs @@ -16,15 +16,11 @@ namespace Artemis.Core /// /// The node the pin collection belongs to /// The name of the pin collection - /// The amount of pins to initially add to the collection /// The resulting output pin collection - protected PinCollection(INode node, string name, int initialCount) + protected PinCollection(INode node, string name) { Node = node ?? throw new ArgumentNullException(nameof(node)); Name = name; - - for (int i = 0; i < initialCount; i++) - AddPin(); } #endregion @@ -61,14 +57,18 @@ namespace Artemis.Core #region Methods /// - public IPin AddPin() + public void Add(IPin pin) { - IPin pin = CreatePin(); + if (pin.Direction != Direction) + throw new ArtemisCoreException($"Can't add a {pin.Direction} pin to an {Direction} pin collection."); + if (pin.Type != Type) + throw new ArtemisCoreException($"Can't add a {pin.Type} pin to an {Type} pin collection."); + + if (_pins.Contains(pin)) + return; + _pins.Add(pin); - PinAdded?.Invoke(this, new SingleValueEventArgs(pin)); - - return pin; } /// @@ -89,11 +89,8 @@ namespace Artemis.Core pin.Reset(); } - /// - /// Creates a new pin to be used in this collection - /// - /// The resulting pin - protected abstract IPin CreatePin(); + /// + public abstract IPin CreatePin(); /// public IEnumerator GetEnumerator() diff --git a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs index 3de441b2f..26c18bcff 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/Wrapper/VisualScriptPinCollection.cs @@ -70,7 +70,7 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper Node.Script.OnScriptUpdated(); } - public void AddPin() => PinCollection.AddPin(); + public void AddPin() => PinCollection.Add(PinCollection.CreatePin()); public void RemovePin(VisualScriptPin pin) => PinCollection.Remove(pin.Pin); @@ -83,4 +83,4 @@ namespace Artemis.VisualScripting.Editor.Controls.Wrapper #endregion } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 09e102ae0..8094f131e 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -62,6 +62,7 @@ public class SelectionRectangle : Control private bool _isSelecting; private IControl? _oldInputElement; private Point _startPosition; + private Point _lastPosition; /// public SelectionRectangle() @@ -168,9 +169,16 @@ public class SelectionRectangle : Control private void ParentOnPointerMoved(object? sender, PointerEventArgs e) { + // Point moved seems to trigger when the element under the mouse changes? + // I'm not sure why this is needed but this check makes sure the position really hasn't changed. + Point position = e.GetCurrentPoint(null).Position; + if (position == _lastPosition) + return; + _lastPosition = position; + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; - + // Capture the pointer and initialize dragging the first time it moves if (!ReferenceEquals(e.Pointer.Captured, this)) { diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs index 2ac132a0f..45b4a9b59 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddNode.cs @@ -13,7 +13,7 @@ public class AddNode : INodeEditorCommand, IDisposable private bool _isRemoved; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The node script the node belongs to. /// The node to delete. diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs new file mode 100644 index 000000000..f6fc2d4f1 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/AddPin.cs @@ -0,0 +1,38 @@ +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to add a pin to a pin collection. +/// +public class AddPin : INodeEditorCommand +{ + private readonly IPinCollection _pinCollection; + private IPin? _pin; + + /// + /// Creates a new instance of the class. + /// + /// The pin collection to add the pin to. + public AddPin(IPinCollection pinCollection) + { + _pinCollection = pinCollection; + } + + /// + public string DisplayName => "Add pin"; + + /// + public void Execute() + { + _pin ??= _pinCollection.CreatePin(); + _pinCollection.Add(_pin); + } + + /// + public void Undo() + { + if (_pin != null) + _pinCollection.Remove(_pin); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs index 7a02da355..6d71769ee 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/ConnectPins.cs @@ -1,14 +1,22 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Artemis.Core; -using Artemis.UI.Shared.Services.NodeEditor; +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to connect two pins. +/// public class ConnectPins : INodeEditorCommand { private readonly IPin _source; private readonly IPin _target; private readonly List? _originalConnections; + /// + /// Creates a new instance of the class. + /// + /// The source of the connection. + /// The target of the connection. public ConnectPins(IPin source, IPin target) { _source = source; diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs index 6c1577427..5886c5993 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/DeleteNode.cs @@ -11,11 +11,11 @@ public class DeleteNode : INodeEditorCommand, IDisposable { private readonly INode _node; private readonly INodeScript _nodeScript; - private readonly Dictionary> _pinConnections = new(); + private readonly Dictionary> _pinConnections = new(); private bool _isRemoved; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The node script the node belongs to. /// The node to delete. @@ -30,7 +30,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable _pinConnections.Clear(); foreach (IPin nodePin in _node.Pins) { - _pinConnections.Add(nodePin, nodePin.ConnectedTo); + _pinConnections.Add(nodePin, new List(nodePin.ConnectedTo)); nodePin.DisconnectAll(); } @@ -38,7 +38,7 @@ public class DeleteNode : INodeEditorCommand, IDisposable { foreach (IPin nodePin in nodePinCollection) { - _pinConnections.Add(nodePin, nodePin.ConnectedTo); + _pinConnections.Add(nodePin, new List(nodePin.ConnectedTo)); nodePin.DisconnectAll(); } } @@ -48,18 +48,22 @@ public class DeleteNode : INodeEditorCommand, IDisposable { foreach (IPin nodePin in _node.Pins) { - if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList? connections)) + if (_pinConnections.TryGetValue(nodePin, out List? connections)) + { foreach (IPin connection in connections) nodePin.ConnectTo(connection); + } } foreach (IPinCollection nodePinCollection in _node.PinCollections) { foreach (IPin nodePin in nodePinCollection) { - if (_pinConnections.TryGetValue(nodePin, out IReadOnlyList? connections)) + if (_pinConnections.TryGetValue(nodePin, out List? connections)) + { foreach (IPin connection in connections) nodePin.ConnectTo(connection); + } } } diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs new file mode 100644 index 000000000..9e4906fe1 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/RemovePin.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to remove a pin from a pin collection. +/// +public class RemovePin : INodeEditorCommand +{ + private readonly IPinCollection _pinCollection; + private readonly IPin _pin; + private readonly List _originalConnections; + + /// + /// Creates a new instance of the class. + /// + /// The pin collection to add the pin to. + /// The pin to remove. + public RemovePin(IPinCollection pinCollection, IPin pin) + { + if (!pinCollection.Contains(pin)) + throw new ArtemisSharedUIException("Can't remove a pin from a collection it isn't contained in."); + + _pinCollection = pinCollection; + _pin = pin; + + _originalConnections = new List(_pin.ConnectedTo); + } + + /// + public string DisplayName => "Remove pin"; + + /// + public void Execute() + { + _pin.DisconnectAll(); + _pinCollection.Remove(_pin); + } + + /// + public void Undo() + { + _pinCollection.Add(_pin); + foreach (IPin originalConnection in _originalConnections) + _pin.ConnectTo(originalConnection); + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml index 012270a2a..51a728924 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Button.axaml @@ -59,6 +59,19 @@ + + + + diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 4bada05d2..e1cde58ae 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -101,9 +101,9 @@ namespace Artemis.UI.Ninject.Factories public interface INodePinVmFactory { - PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection); + PinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel); PinViewModel InputPinViewModel(IPin inputPin); - PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection); + PinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel); PinViewModel OutputPinViewModel(IPin outputPin); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml index c60dda5ff..b42c97c1d 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml @@ -4,32 +4,78 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:skiaSharp="clr-namespace:SkiaSharp;assembly=SkiaSharp" + xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.VisualScripting.CableView" x:DataType="visualScripting:CableViewModel" - ClipToBounds="False"> + ClipToBounds="False" + IsVisible="{CompiledBinding Connected}"> + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs index c8f92e883..4d29600d2 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableView.axaml.cs @@ -15,15 +15,19 @@ public class CableView : ReactiveUserControl { private const double CABLE_OFFSET = 24 * 4; private readonly Path _cablePath; + private readonly Border _valueBorder; public CableView() { InitializeComponent(); _cablePath = this.Get("CablePath"); + _valueBorder = this.Get("ValueBorder"); // Not using bindings here to avoid a warnings this.WhenActivated(d => { + _valueBorder.GetObservable(BoundsProperty).Subscribe(rect => _valueBorder.RenderTransform = new TranslateTransform(rect.Width / 2 * -1, rect.Height / 2 * -1)).DisposeWith(d); + ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d); ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d); Update(); diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs index 38991d633..2a872e8c0 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/CableViewModel.cs @@ -15,13 +15,15 @@ namespace Artemis.UI.Screens.VisualScripting; public class CableViewModel : ActivatableViewModelBase { + private readonly ObservableAsPropertyHelper _connected; private readonly ObservableAsPropertyHelper _cableColor; private readonly ObservableAsPropertyHelper _fromPoint; private readonly ObservableAsPropertyHelper _toPoint; + private readonly ObservableAsPropertyHelper _valuePoint; private PinViewModel? _fromViewModel; private PinViewModel? _toViewModel; - + public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to) { if (from.Direction != PinDirection.Output) @@ -44,10 +46,19 @@ public class CableViewModel : ActivatableViewModelBase .Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never()) .Switch() .ToProperty(this, vm => vm.ToPoint); + _valuePoint = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint).Select(tuple => new Point( + tuple.Item1.X + (tuple.Item2.X - tuple.Item1.X) / 2, + tuple.Item1.Y + (tuple.Item2.Y - tuple.Item1.Y) / 2 + )).ToProperty(this, vm => vm.ValuePoint); _cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel) .Select(tuple => tuple.Item1?.PinColor ?? tuple.Item2?.PinColor ?? new Color(255, 255, 255, 255)) .ToProperty(this, vm => vm.CableColor); + + // Not a perfect solution but this makes sure the cable never renders at 0,0 (can happen when the cable spawns before the pin ever rendered) + _connected = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint) + .Select(tuple => tuple.Item1 != new Point(0, 0) && tuple.Item2 != new Point(0, 0)) + .ToProperty(this, vm => vm.Connected); } public PinViewModel? FromViewModel @@ -62,7 +73,10 @@ public class CableViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _toViewModel, value); } + public bool Connected => _connected.Value; + public Point FromPoint => _fromPoint.Value; public Point ToPoint => _toPoint.Value; + public Point ValuePoint => _valuePoint.Value; public Color CableColor => _cableColor.Value; } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index 37aa77d57..ce07fef19 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -10,13 +10,15 @@ - - - - - - - + PointerMoved="InputElement_OnPointerMoved" + ClipToBounds="True" + Background="{DynamicResource ContentDialogBackground}"> + + + + + + + + - + - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 144b76d5a..9cb3dab1e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Reactive; using System.Reactive.Linq; using Artemis.Core; @@ -25,10 +26,13 @@ public class NodeViewModel : ActivatableViewModelBase private double _dragOffsetX; private double _dragOffsetY; private bool _isSelected; - private ObservableAsPropertyHelper? _isStaticNode; private double _startX; private double _startY; + private ObservableAsPropertyHelper? _isStaticNode; + private ObservableAsPropertyHelper? _hasInputPins; + private ObservableAsPropertyHelper? _hasOutputPins; + public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { NodeScriptViewModel = nodeScriptViewModel; @@ -57,12 +61,12 @@ public class NodeViewModel : ActivatableViewModelBase // Same again but for pin collections nodePinCollections.Connect() .Filter(n => n.Direction == PinDirection.Input) - .Transform(nodePinVmFactory.InputPinCollectionViewModel) + .Transform(c => nodePinVmFactory.InputPinCollectionViewModel(c, nodeScriptViewModel)) .Bind(out ReadOnlyObservableCollection inputPinCollections) .Subscribe(); nodePinCollections.Connect() .Filter(n => n.Direction == PinDirection.Output) - .Transform(nodePinVmFactory.OutputPinCollectionViewModel) + .Transform(c => nodePinVmFactory.OutputPinCollectionViewModel(c, nodeScriptViewModel)) .Bind(out ReadOnlyObservableCollection outputPinCollections) .Subscribe(); InputPinCollectionViewModels = inputPinCollections; @@ -84,6 +88,16 @@ public class NodeViewModel : ActivatableViewModelBase .Select(tuple => tuple.Item1 || tuple.Item2) .ToProperty(this, model => model.IsStaticNode) .DisposeWith(d); + _hasInputPins = InputPinViewModels.ToObservableChangeSet() + .Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Any() + .ToProperty(this, vm => vm.HasInputPins) + .DisposeWith(d); + _hasOutputPins = OutputPinViewModels.ToObservableChangeSet() + .Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels)) + .Any() + .ToProperty(this, vm => vm.HasOutputPins) + .DisposeWith(d); // Subscribe to pin changes Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source => @@ -97,10 +111,15 @@ public class NodeViewModel : ActivatableViewModelBase source.Clear(); source.AddRange(c); })).DisposeWith(d); + + if (Node is Node coreNode) + CustomNodeViewModel = coreNode.GetCustomViewModel(); }); } public bool IsStaticNode => _isStaticNode?.Value ?? true; + public bool HasInputPins => _hasInputPins?.Value ?? false; + public bool HasOutputPins => _hasOutputPins?.Value ?? false; public NodeScriptViewModel NodeScriptViewModel { get; } public INode Node { get; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml index a6a0f7e98..2e313b667 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionView.axaml @@ -13,7 +13,20 @@ Command="{CompiledBinding AddPin}"> - + + + + + + + + + + - diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs index 0edea5c81..cbaf8145a 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/InputPinCollectionViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared.Services.NodeEditor; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -7,7 +8,8 @@ public class InputPinCollectionViewModel : PinCollectionViewModel { public InputPinCollection InputPinCollection { get; } - public InputPinCollectionViewModel(InputPinCollection inputPinCollection, INodePinVmFactory nodePinVmFactory) : base(inputPinCollection, nodePinVmFactory) + public InputPinCollectionViewModel(InputPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) + : base(inputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService) { InputPinCollection = inputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml index 091246cd8..43d3bfbde 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionView.axaml @@ -14,6 +14,20 @@ Command="{CompiledBinding AddPin}"> - + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs index c268a3bfd..6a9d86b8e 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/OutputPinCollectionViewModel.cs @@ -1,5 +1,6 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Shared.Services.NodeEditor; namespace Artemis.UI.Screens.VisualScripting.Pins; @@ -7,7 +8,8 @@ public class OutputPinCollectionViewModel : PinCollectionViewModel { public OutputPinCollection OutputPinCollection { get; } - public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection, INodePinVmFactory nodePinVmFactory) : base(outputPinCollection, nodePinVmFactory) + public OutputPinCollectionViewModel(OutputPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) + : base(outputPinCollection, nodeScriptViewModel, nodePinVmFactory, nodeEditorService) { OutputPinCollection = outputPinCollection; } diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs index e521ecc8e..44b199fa2 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinCollectionViewModel.cs @@ -7,6 +7,8 @@ using Artemis.Core; using Artemis.Core.Events; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia.Controls.Mixins; using DynamicData; using ReactiveUI; @@ -16,10 +18,8 @@ namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinCollectionViewModel : ActivatableViewModelBase { private readonly INodePinVmFactory _nodePinVmFactory; - public IPinCollection PinCollection { get; } - public ObservableCollection PinViewModels { get; } - protected PinCollectionViewModel(IPinCollection pinCollection, INodePinVmFactory nodePinVmFactory) + protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) { _nodePinVmFactory = nodePinVmFactory; @@ -39,13 +39,20 @@ public abstract class PinCollectionViewModel : ActivatableViewModelBase .DisposeWith(d); }); - AddPin = ReactiveCommand.Create(() => PinCollection.AddPin()); + AddPin = ReactiveCommand.Create(() => nodeEditorService.ExecuteCommand(nodeScriptViewModel.NodeScript, new AddPin(pinCollection))); + RemovePin = ReactiveCommand.Create((IPin pin) => nodeEditorService.ExecuteCommand(nodeScriptViewModel.NodeScript, new RemovePin(pinCollection, pin))); } - public ReactiveCommand AddPin { get; } + public IPinCollection PinCollection { get; } + public ReactiveCommand AddPin { get; } + public ReactiveCommand RemovePin { get; } + + public ObservableCollection PinViewModels { get; } private PinViewModel CreatePinViewModel(IPin pin) { - return PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin); + PinViewModel vm = PinCollection.Direction == PinDirection.Input ? _nodePinVmFactory.InputPinViewModel(pin) : _nodePinVmFactory.OutputPinViewModel(pin); + vm.RemovePin = RemovePin; + return vm; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs index 099936779..5d7cb3bb5 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs @@ -78,12 +78,12 @@ public class PinView : ReactiveUserControl private void UpdatePosition() { - if (_container == null || ViewModel == null) + if (_container == null || _pinPoint == null || ViewModel == null) return; - Matrix? transform = this.TransformToVisual(_container); + Matrix? transform = _pinPoint.TransformToVisual(_container); if (transform != null) - ViewModel.Position = new Point(Bounds.Width / 2, Bounds.Height / 2).Transform(transform.Value); + ViewModel.Position = new Point(_pinPoint.Bounds.Width / 2, _pinPoint.Bounds.Height / 2).Transform(transform.Value); } #endregion diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs index ba6ad4d4e..5133fef3c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive; using System.Reactive.Linq; using Artemis.Core; using Artemis.Core.Events; @@ -16,6 +17,7 @@ namespace Artemis.UI.Screens.VisualScripting.Pins; public abstract class PinViewModel : ActivatableViewModelBase { private Point _position; + private ReactiveCommand? _removePin; protected PinViewModel(IPin pin, INodeService nodeService) { @@ -52,6 +54,12 @@ public abstract class PinViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _position, value); } + public ReactiveCommand? RemovePin + { + get => _removePin; + set => RaiseAndSetIfChanged(ref _removePin, value); + } + public bool IsCompatibleWith(PinViewModel pinViewModel) { if (pinViewModel.Pin.Direction == Pin.Direction) diff --git a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs index 930f8bdf0..0aab5dd47 100644 --- a/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Workshop/WorkshopViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Ninject.Factories; @@ -9,6 +10,7 @@ using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Interfaces; using Artemis.VisualScripting.Nodes; using Avalonia.Input; +using Avalonia.Threading; using ReactiveUI; using SkiaSharp; @@ -16,6 +18,8 @@ namespace Artemis.UI.Screens.Workshop { public class WorkshopViewModel : MainScreenViewModel { + private static NodeScript? _testScript = null; + private readonly INotificationService _notificationService; private StandardCursorType _selectedCursor; private readonly ObservableAsPropertyHelper _cursor; @@ -38,16 +42,26 @@ namespace Artemis.UI.Screens.Workshop DisplayName = "Workshop"; ShowNotification = ReactiveCommand.Create(ExecuteShowNotification); - NodeScript testScript = new("Test script", "A test script"); - INode exitNode = testScript.Nodes.Last(); - exitNode.X = 300; - exitNode.Y = 150; + if (_testScript == null) + { + _testScript = new NodeScript("Test script", "A test script"); + INode exitNode = _testScript.Nodes.Last(); + exitNode.X = 300; + exitNode.Y = 150; - OrNode orNode = new() {X = 100, Y = 100}; - testScript.AddNode(orNode); - orNode.Result.ConnectTo(exitNode.Pins.First()); + OrNode orNode = new() {X = 100, Y = 100}; + _testScript.AddNode(orNode); + orNode.Result.ConnectTo(exitNode.Pins.First()); + } - VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(testScript); + VisualEditorViewModel = nodeVmFactory.NodeScriptViewModel(_testScript); + + this.WhenActivated(d => + { + DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(20), DispatcherPriority.Normal, (_, _) => _testScript?.Run()); + updateTimer.Start(); + Disposable.Create(() => updateTimer.Stop()).DisposeWith(d); + }); } public NodeScriptViewModel VisualEditorViewModel { get; } diff --git a/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs b/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs new file mode 100644 index 000000000..4673f2912 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using Artemis.Core; +using Avalonia.Data.Converters; + +namespace Artemis.VisualScripting.Converters; + +/// +/// Converts input into . +/// +public class NumericConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(Numeric)) + return new Numeric(value); + + return value; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (targetType == typeof(Numeric)) + return new Numeric(value); + + return value; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml deleted file mode 100644 index da9a0378f..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/EnumEqualsNodeCustomView.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml deleted file mode 100644 index 2b526f152..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/LayerPropertyNodeCustomView.xaml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml new file mode 100644 index 000000000..53052b696 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs new file mode 100644 index 000000000..ea3efe812 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.VisualScripting.Nodes.CustomViews +{ + public partial class StaticNumericValueNodeCustomView : UserControl + { + public StaticNumericValueNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml deleted file mode 100644 index 6c43dfcbf..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml deleted file mode 100644 index 54335410e..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.xaml +++ /dev/null @@ -1,11 +0,0 @@ - - - \ No newline at end of file From 81ca8c1425813ecbb54f3aebdb1a9f8d2acabc56 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 20 Mar 2022 23:08:06 +0100 Subject: [PATCH 151/270] Node editor - Added static input nodes, easing type node Data model picker - Added initial control --- .../Controls/DataModelPicker.axaml | 16 + .../Controls/DataModelPicker.axaml.cs | 290 ++++++++++++++++++ .../Controls/SelectionRectangle.cs | 1 + .../Events/DataModelSelectedEventArgs.cs | 22 ++ .../Screens/VisualScripting/NodeView.axaml | 8 +- .../Screens/VisualScripting/NodeView.axaml.cs | 5 + .../Artemis.VisualScripting.csproj | 6 + .../StaticNumericValueNodeCustomView.axaml | 7 +- .../StaticStringValueNodeCustomView.axaml | 10 + .../StaticStringValueNodeCustomView.axaml.cs | 18 ++ .../DataModelEventNodeCustomViewModel.cs | 59 ++-- .../DataModelNodeCustomViewModel.cs | 59 ++-- .../DataModelEventNodeCustomView.axaml | 8 + .../DataModelEventNodeCustomView.axaml.cs | 19 ++ .../EasingTypeNodeCustomView.axaml | 9 + .../EasingTypeNodeCustomView.axaml.cs | 19 ++ .../CustomViews/EasingTypeNodeCustomView.xaml | 34 -- 17 files changed, 489 insertions(+), 101 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml create mode 100644 src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs create mode 100644 src/Avalonia/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml create mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs delete mode 100644 src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml new file mode 100644 index 000000000..4fee1e71a --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs new file mode 100644 index 000000000..651d0d48c --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using Artemis.Core; +using Artemis.Core.Modules; +using Artemis.UI.Shared.DataModelVisualization.Shared; +using Artemis.UI.Shared.Events; +using Artemis.UI.Shared.Services.Interfaces; +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; +using ReactiveUI; + +namespace Artemis.UI.Shared.Controls; + +public class DataModelPicker : TemplatedControl +{ + private static IDataModelUIService? _dataModelUIService; + + /// + /// Gets or sets data model path. + /// + public static readonly StyledProperty DataModelPathProperty = + AvaloniaProperty.Register(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay, notifying: DataModelPathPropertyChanged); + + /// + /// Gets or sets the placeholder to show when nothing is selected. + /// + public static readonly StyledProperty PlaceholderProperty = + AvaloniaProperty.Register(nameof(Placeholder), "Click to select"); + + /// + /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. + /// + public static readonly StyledProperty ShowDataModelValuesProperty = + AvaloniaProperty.Register(nameof(ShowDataModelValues)); + + /// + /// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value. + /// + public static readonly StyledProperty ShowFullPathProperty = + AvaloniaProperty.Register(nameof(ShowFullPath), notifying: ShowFullPathPropertyChanged); + + /// + /// Gets or sets the brush to use when drawing the button. + /// + public static readonly StyledProperty ButtonBrushProperty = + AvaloniaProperty.Register(nameof(ButtonBrush)); + + /// + /// A list of extra modules to show data models of. + /// + public static readonly StyledProperty?> ModulesProperty = + AvaloniaProperty.Register?>(nameof(Modules), new ObservableCollection(), notifying: ModulesPropertyChanged); + + /// + /// The data model view model to show, if not provided one will be retrieved by the control. + /// + public static readonly StyledProperty DataModelViewModelProperty = + AvaloniaProperty.Register(nameof(DataModelViewModel), notifying: DataModelViewModelPropertyChanged); + + /// + /// A list of data model view models to show + /// + public static readonly StyledProperty?> ExtraDataModelViewModelsProperty = + AvaloniaProperty.Register?>( + nameof(ExtraDataModelViewModels), + new ObservableCollection(), + notifying: ExtraDataModelViewModelsPropertyChanged + ); + + /// + /// A list of types to filter the selectable paths on. + /// + public static readonly StyledProperty?> FilterTypesProperty = + AvaloniaProperty.Register?>(nameof(FilterTypes), new ObservableCollection()); + + /// + /// Creates a new instance of the class. + /// + public DataModelPicker() + { + SelectPropertyCommand = ReactiveCommand.Create(selected => ExecuteSelectPropertyCommand(selected)); + } + + /// + /// Gets a command that selects the path by it's view model. + /// + public ReactiveCommand SelectPropertyCommand { get; } + + internal static IDataModelUIService DataModelUIService + { + set + { + if (_dataModelUIService != null) + throw new AccessViolationException("This is not for you to touch"); + _dataModelUIService = value; + } + } + + /// + /// Gets or sets data model path. + /// + public DataModelPath? DataModelPath + { + get => GetValue(DataModelPathProperty); + set => SetValue(DataModelPathProperty, value); + } + + /// + /// Gets or sets the placeholder to show when nothing is selected. + /// + public string Placeholder + { + get => GetValue(PlaceholderProperty); + set => SetValue(PlaceholderProperty, value); + } + + /// + /// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value. + /// + public bool ShowFullPath + { + get => GetValue(ShowFullPathProperty); + set => SetValue(ShowFullPathProperty, value); + } + + /// + /// Gets or sets a boolean indicating whether the data model picker should show current values when selecting a path. + /// + public bool ShowDataModelValues + { + get => GetValue(ShowDataModelValuesProperty); + set => SetValue(ShowDataModelValuesProperty, value); + } + + /// + /// Gets or sets the brush to use when drawing the button. + /// + public Brush ButtonBrush + { + get => GetValue(ButtonBrushProperty); + set => SetValue(ButtonBrushProperty, value); + } + + /// + /// A list of extra modules to show data models of. + /// + public ObservableCollection? Modules + { + get => GetValue(ModulesProperty); + set => SetValue(ModulesProperty, value); + } + + /// + /// The data model view model to show, if not provided one will be retrieved by the control. + /// + public DataModelPropertiesViewModel? DataModelViewModel + { + get => GetValue(DataModelViewModelProperty); + set => SetValue(DataModelViewModelProperty, value); + } + + /// + /// A list of data model view models to show. + /// + public ObservableCollection? ExtraDataModelViewModels + { + get => GetValue(ExtraDataModelViewModelsProperty); + set => SetValue(ExtraDataModelViewModelsProperty, value); + } + + /// + /// A list of types to filter the selectable paths on. + /// + public ObservableCollection? FilterTypes + { + get => GetValue(FilterTypesProperty); + set => SetValue(FilterTypesProperty, value); + } + + /// + /// Occurs when a new path has been selected + /// + public event EventHandler? DataModelPathSelected; + + /// + /// Invokes the event + /// + /// + protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e) + { + DataModelPathSelected?.Invoke(this, e); + } + + private void ExecuteSelectPropertyCommand(DataModelVisualizationViewModel selected) + { + if (selected.DataModelPath == null) + return; + if (selected.DataModelPath.Equals(DataModelPath)) + return; + + DataModelPath = new DataModelPath(selected.DataModelPath); + OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath)); + } + + private void GetDataModel() + { + if (_dataModelUIService == null) + return; + + ChangeDataModel(_dataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List(), true)); + } + + private void ChangeDataModel(DataModelPropertiesViewModel? dataModel) + { + if (DataModelViewModel != null) + { + DataModelViewModel.Dispose(); + DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested; + } + + DataModelViewModel = dataModel; + if (DataModelViewModel != null) + DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; + } + + #region Overrides of Visual + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + } + + #endregion + + private void UpdateValueDisplay() + { + ValueDisplay.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Visible : Visibility.Collapsed; + ValuePlaceholder.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Collapsed : Visibility.Visible; + + string? formattedPath = null; + if (DataModelPath != null && DataModelPath.IsValid) + formattedPath = string.Join(" ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name)); + + DataModelButton.ToolTip = formattedPath; + ValueDisplayTextBlock.Text = ShowFullPath + ? formattedPath + : DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier; + } + + private void DataModelOnUpdateRequested(object? sender, EventArgs e) + { + DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); + if (ExtraDataModelViewModels == null) return; + foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) + extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); + } + + private static void DataModelPathPropertyChanged(IAvaloniaObject sender, bool before) + { + } + + private static void ShowFullPathPropertyChanged(IAvaloniaObject sender, bool before) + { + } + + private static void ModulesPropertyChanged(IAvaloniaObject sender, bool before) + { + } + + private static void DataModelViewModelPropertyChanged(IAvaloniaObject sender, bool before) + { + } + + private static void ExtraDataModelViewModelsPropertyChanged(IAvaloniaObject sender, bool before) + { + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index 8094f131e..5c43e5cdb 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -174,6 +174,7 @@ public class SelectionRectangle : Control Point position = e.GetCurrentPoint(null).Position; if (position == _lastPosition) return; + _lastPosition = position; if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) diff --git a/src/Avalonia/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs b/src/Avalonia/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs new file mode 100644 index 000000000..7f59aedad --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs @@ -0,0 +1,22 @@ +using System; +using Artemis.Core; +using Artemis.UI.Shared.Controls; + +namespace Artemis.UI.Shared.Events +{ + /// + /// Provides data about selection events raised by + /// + public class DataModelSelectedEventArgs : EventArgs + { + /// + /// Gets the data model path that was selected + /// + public DataModelPath? Path { get; } + + internal DataModelSelectedEventArgs(DataModelPath? path) + { + Path = path; + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index ce07fef19..d7b1fb86b 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -26,11 +26,8 @@ - - + @@ -69,7 +65,7 @@ - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index be0b892dc..6e0322bde 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -85,4 +85,9 @@ public class NodeView : ReactiveUserControl e.Handled = true; } + + private void NodeContainer_OnPointerMoved(object? sender, PointerEventArgs e) + { + e.Handled = true; + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj index be4170e96..d6394a424 100644 --- a/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -71,4 +71,10 @@ + + + StaticStringValueNodeCustomView.axaml + + + diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml index 53052b696..f4ef79c1a 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml @@ -4,11 +4,16 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.CustomViewModels" xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView" x:DataType="customViewModels:StaticNumericValueNodeCustomViewModel"> - + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml new file mode 100644 index 000000000..966921331 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs new file mode 100644 index 000000000..c619b4b29 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.VisualScripting.Nodes.CustomViews +{ + public partial class StaticStringValueNodeCustomView : UserControl + { + public StaticStringValueNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs index daeb894dc..8ebb713a4 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs @@ -1,15 +1,18 @@ using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Shared.VisualScripting; +using ReactiveUI; namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; public class DataModelEventNodeCustomViewModel : CustomNodeViewModel { private readonly DataModelEventNode _node; - private ObservableCollection _modules; + private ObservableCollection? _modules; public DataModelEventNodeCustomViewModel(DataModelEventNode node, ISettingsService settingsService) : base(node) { @@ -17,17 +20,32 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + + this.WhenActivated(d => + { + if (Modules != null) + return; + + Modules = new ObservableCollection(); + if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + Modules.Add(scriptProfile.Configuration.Module); + else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + Modules.Add(profileConfiguration.Module); + + _node.PropertyChanged += NodeOnPropertyChanged; + Disposable.Create(() => _node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d); + }); } public PluginSetting ShowFullPaths { get; } public PluginSetting ShowDataModelValues { get; } public ObservableCollection FilterTypes { get; } = new() {typeof(IDataModelEvent)}; - // public ObservableCollection Modules - // { - // get => _modules; - // set => SetAndNotify(ref _modules, value); - // } + public ObservableCollection? Modules + { + get => _modules; + set => RaiseAndSetIfChanged(ref _modules, value); + } public DataModelPath DataModelPath { @@ -45,28 +63,9 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel } } - // public override void OnActivate() - // { - // if (Modules != null) - // return; - // - // Modules = new ObservableCollection(); - // if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) - // Modules.Add(scriptProfile.Configuration.Module); - // else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) - // Modules.Add(profileConfiguration.Module); - // - // _node.PropertyChanged += NodeOnPropertyChanged; - // } - // - // public override void OnDeactivate() - // { - // _node.PropertyChanged -= NodeOnPropertyChanged; - // } - // - // private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) - // { - // if (e.PropertyName == nameof(DataModelNode.DataModelPath)) - // OnPropertyChanged(nameof(DataModelPath)); - // } + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(DataModelNode.DataModelPath)) + this.RaisePropertyChanged(nameof(DataModelPath)); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs index f62bc7533..b3a20bec0 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs @@ -1,15 +1,18 @@ using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; using Artemis.UI.Shared.VisualScripting; +using ReactiveUI; namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; public class DataModelNodeCustomViewModel : CustomNodeViewModel { private readonly DataModelNode _node; - private ObservableCollection _modules; + private ObservableCollection? _modules; public DataModelNodeCustomViewModel(DataModelNode node, ISettingsService settingsService) : base(node) { @@ -17,16 +20,31 @@ public class DataModelNodeCustomViewModel : CustomNodeViewModel ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + + this.WhenActivated(d => + { + if (Modules != null) + return; + + Modules = new ObservableCollection(); + if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + Modules.Add(scriptProfile.Configuration.Module); + else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + Modules.Add(profileConfiguration.Module); + + _node.PropertyChanged += NodeOnPropertyChanged; + Disposable.Create(() => _node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d); + }); } public PluginSetting ShowFullPaths { get; } public PluginSetting ShowDataModelValues { get; } - // public ObservableCollection Modules - // { - // get => _modules; - // set => SetAndNotify(ref _modules, value); - // } + public ObservableCollection? Modules + { + get => _modules; + set => RaiseAndSetIfChanged(ref _modules, value); + } public DataModelPath DataModelPath { @@ -45,28 +63,9 @@ public class DataModelNodeCustomViewModel : CustomNodeViewModel } } - // public override void OnActivate() - // { - // if (Modules != null) - // return; - // - // Modules = new ObservableCollection(); - // if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) - // Modules.Add(scriptProfile.Configuration.Module); - // else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) - // Modules.Add(profileConfiguration.Module); - // - // _node.PropertyChanged += NodeOnPropertyChanged; - // } - // - // public override void OnDeactivate() - // { - // _node.PropertyChanged -= NodeOnPropertyChanged; - // } - // - // private void NodeOnPropertyChanged(object sender, PropertyChangedEventArgs e) - // { - // if (e.PropertyName == nameof(DataModelNode.DataModelPath)) - // OnPropertyChanged(nameof(DataModelPath)); - // } + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(DataModelNode.DataModelPath)) + this.RaisePropertyChanged(nameof(DataModelPath)); + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml new file mode 100644 index 000000000..4a13e9c5f --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml @@ -0,0 +1,8 @@ + + + diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs new file mode 100644 index 000000000..18a8d72de --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.VisualScripting.Nodes.DataModel.CustomViews +{ + public partial class DataModelEventNodeCustomView : UserControl + { + public DataModelEventNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml new file mode 100644 index 000000000..c4154fe81 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml @@ -0,0 +1,9 @@ + + + diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs new file mode 100644 index 000000000..7a699a2a5 --- /dev/null +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.VisualScripting.Nodes.Easing.CustomViews +{ + public partial class EasingTypeNodeCustomView : UserControl + { + public EasingTypeNodeCustomView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml deleted file mode 100644 index 1930207eb..000000000 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.xaml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file From 4cd596602f6678c9782ffac48beca588b56b883e Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 21 Mar 2022 23:08:07 +0100 Subject: [PATCH 152/270] Data model picker - Implemented most of the picker --- .../Controls/DataModelPicker.axaml | 42 ++++- .../Controls/DataModelPicker.axaml.cs | 164 ++++++++++++++---- .../Shared/DataModelVisualizationViewModel.cs | 2 +- .../Artemis.UI.Shared/Styles/Artemis.axaml | 1 + .../Artemis.UI/ArtemisBootstrapper.cs | 3 + .../Screens/Workshop/WorkshopView.axaml | 3 +- 6 files changed, 177 insertions(+), 38 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml index 4fee1e71a..56783710c 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml @@ -1,6 +1,8 @@ + xmlns:controls="using:Artemis.UI.Shared.Controls" + xmlns:fluent="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared"> @@ -9,7 +11,43 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs index 651d0d48c..b469d9058 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker.axaml.cs @@ -9,9 +9,11 @@ using Artemis.UI.Shared.DataModelVisualization.Shared; using Artemis.UI.Shared.Events; using Artemis.UI.Shared.Services.Interfaces; using Avalonia; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Media; +using Avalonia.Threading; using ReactiveUI; namespace Artemis.UI.Shared.Controls; @@ -24,7 +26,7 @@ public class DataModelPicker : TemplatedControl /// Gets or sets data model path. /// public static readonly StyledProperty DataModelPathProperty = - AvaloniaProperty.Register(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay, notifying: DataModelPathPropertyChanged); + AvaloniaProperty.Register(nameof(DataModelPath), defaultBindingMode: BindingMode.TwoWay); /// /// Gets or sets the placeholder to show when nothing is selected. @@ -42,7 +44,7 @@ public class DataModelPicker : TemplatedControl /// Gets or sets a boolean indicating whether the data model picker should show the full path of the selected value. /// public static readonly StyledProperty ShowFullPathProperty = - AvaloniaProperty.Register(nameof(ShowFullPath), notifying: ShowFullPathPropertyChanged); + AvaloniaProperty.Register(nameof(ShowFullPath)); /// /// Gets or sets the brush to use when drawing the button. @@ -54,23 +56,19 @@ public class DataModelPicker : TemplatedControl /// A list of extra modules to show data models of. /// public static readonly StyledProperty?> ModulesProperty = - AvaloniaProperty.Register?>(nameof(Modules), new ObservableCollection(), notifying: ModulesPropertyChanged); + AvaloniaProperty.Register?>(nameof(Modules), new ObservableCollection()); /// /// The data model view model to show, if not provided one will be retrieved by the control. /// public static readonly StyledProperty DataModelViewModelProperty = - AvaloniaProperty.Register(nameof(DataModelViewModel), notifying: DataModelViewModelPropertyChanged); + AvaloniaProperty.Register(nameof(DataModelViewModel)); /// /// A list of data model view models to show /// public static readonly StyledProperty?> ExtraDataModelViewModelsProperty = - AvaloniaProperty.Register?>( - nameof(ExtraDataModelViewModels), - new ObservableCollection(), - notifying: ExtraDataModelViewModelsPropertyChanged - ); + AvaloniaProperty.Register?>(nameof(ExtraDataModelViewModels), new ObservableCollection()); /// /// A list of types to filter the selectable paths on. @@ -78,6 +76,70 @@ public class DataModelPicker : TemplatedControl public static readonly StyledProperty?> FilterTypesProperty = AvaloniaProperty.Register?>(nameof(FilterTypes), new ObservableCollection()); + /// + /// Gets a boolean indicating whether the data model picker has a value. + /// + public static readonly StyledProperty HasValueProperty = + AvaloniaProperty.Register(nameof(HasValue)); + + private Button? _dataModelButton; + private bool _attached; + + static DataModelPicker() + { + DataModelPathProperty.Changed.Subscribe(DataModelPathChanged); + ShowFullPathProperty.Changed.Subscribe(ShowFullPathChanged); + ModulesProperty.Changed.Subscribe(ModulesChanged); + DataModelViewModelProperty.Changed.Subscribe(DataModelViewModelPropertyChanged); + ExtraDataModelViewModelsProperty.Changed.Subscribe(ExtraDataModelViewModelsChanged); + } + + private static void DataModelPathChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is not DataModelPicker dataModelPicker) + return; + + if (e.OldValue.Value != null) + { + e.OldValue.Value.PathInvalidated -= dataModelPicker.PathValidationChanged; + e.OldValue.Value.PathValidated -= dataModelPicker.PathValidationChanged; + e.OldValue.Value.Dispose(); + } + + if (!dataModelPicker._attached) + return; + + dataModelPicker.UpdateValueDisplay(); + if (e.NewValue.Value != null) + { + e.NewValue.Value.PathInvalidated += dataModelPicker.PathValidationChanged; + e.NewValue.Value.PathValidated += dataModelPicker.PathValidationChanged; + } + } + + private static void ShowFullPathChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is DataModelPicker dataModelPicker) + dataModelPicker.UpdateValueDisplay(); + } + + private static void ModulesChanged(AvaloniaPropertyChangedEventArgs?> e) + { + if (e.Sender is DataModelPicker dataModelPicker) + dataModelPicker.GetDataModel(); + } + + private static void DataModelViewModelPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is DataModelPicker && e.OldValue.Value != null) + e.OldValue.Value.Dispose(); + } + + private static void ExtraDataModelViewModelsChanged(AvaloniaPropertyChangedEventArgs?> e) + { + // TODO, the original did nothing here either and I can't remember why + } + /// /// Creates a new instance of the class. /// @@ -91,7 +153,10 @@ public class DataModelPicker : TemplatedControl /// public ReactiveCommand SelectPropertyCommand { get; } - internal static IDataModelUIService DataModelUIService + /// + /// Internal, don't use. + /// + public static IDataModelUIService DataModelUIService { set { @@ -182,6 +247,15 @@ public class DataModelPicker : TemplatedControl set => SetValue(FilterTypesProperty, value); } + /// + /// Gets a boolean indicating whether the data model picker has a value. + /// + public bool HasValue + { + get => GetValue(HasValueProperty); + private set => SetValue(HasValueProperty, value); + } + /// /// Occurs when a new path has been selected /// @@ -228,18 +302,45 @@ public class DataModelPicker : TemplatedControl DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; } + #region Overrides of TemplatedControl + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _dataModelButton = e.NameScope.Find - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DataModelPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/DataModelPicker.xaml.cs deleted file mode 100644 index 84d3b4d33..000000000 --- a/src/Artemis.UI.Shared/Controls/DataModelPicker.xaml.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Threading; -using Artemis.Core; -using Artemis.Core.Modules; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Shared.Controls -{ - /// - /// Interaction logic for DataModelPicker.xaml - /// - public partial class DataModelPicker : INotifyPropertyChanged - { - private static IDataModelUIService _dataModelUIService; - - /// - /// Gets or sets data model path - /// - public static readonly DependencyProperty DataModelPathProperty = DependencyProperty.Register( - nameof(DataModelPath), typeof(DataModelPath), typeof(DataModelPicker), - new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, DataModelPathPropertyChangedCallback) - ); - - /// - /// Gets or sets the brush to use when drawing the button - /// - public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register( - nameof(Placeholder), typeof(string), typeof(DataModelPicker), - new FrameworkPropertyMetadata("Click to select") - ); - - /// - /// Gets or sets the brush to use when drawing the button - /// - public static readonly DependencyProperty ShowDataModelValuesProperty = DependencyProperty.Register(nameof(ShowDataModelValues), typeof(bool), typeof(DataModelPicker)); - - /// - /// Gets or sets the brush to use when drawing the button - /// - public static readonly DependencyProperty ShowFullPathProperty = DependencyProperty.Register( - nameof(ShowFullPath), typeof(bool), typeof(DataModelPicker), - new FrameworkPropertyMetadata(ShowFullPathPropertyCallback) - ); - - /// - /// Gets or sets the brush to use when drawing the button - /// - public static readonly DependencyProperty ButtonBrushProperty = DependencyProperty.Register(nameof(ButtonBrush), typeof(Brush), typeof(DataModelPicker)); - - /// - /// A list of extra modules to show data models of - /// - public static readonly DependencyProperty ModulesProperty = DependencyProperty.Register( - nameof(Modules), typeof(ObservableCollection), typeof(DataModelPicker), - new FrameworkPropertyMetadata(new ObservableCollection(), FrameworkPropertyMetadataOptions.None, ModulesPropertyChangedCallback) - ); - - /// - /// The data model view model to show, if not provided one will be retrieved by the control - /// - public static readonly DependencyProperty DataModelViewModelProperty = DependencyProperty.Register( - nameof(DataModelViewModel), typeof(DataModelPropertiesViewModel), typeof(DataModelPicker), - new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, DataModelViewModelPropertyChangedCallback) - ); - - /// - /// A list of data model view models to show - /// - public static readonly DependencyProperty ExtraDataModelViewModelsProperty = DependencyProperty.Register( - nameof(ExtraDataModelViewModels), typeof(ObservableCollection), typeof(DataModelPicker), - new FrameworkPropertyMetadata(new ObservableCollection(), FrameworkPropertyMetadataOptions.None, ExtraDataModelViewModelsPropertyChangedCallback) - ); - - /// - /// A list of data model view models to show - /// - public static readonly DependencyProperty FilterTypesProperty = DependencyProperty.Register( - nameof(FilterTypes), typeof(ObservableCollection), typeof(DataModelPicker), - new FrameworkPropertyMetadata(new ObservableCollection()) - ); - - public DataModelPicker() - { - SelectPropertyCommand = new DelegateCommand(ExecuteSelectPropertyCommand); - Unloaded += OnUnloaded; - InitializeComponent(); - GetDataModel(); - UpdateValueDisplay(); - } - - /// - /// Gets or sets the brush to use when drawing the button - /// - public DataModelPath? DataModelPath - { - get => (DataModelPath) GetValue(DataModelPathProperty); - set => SetValue(DataModelPathProperty, value); - } - - /// - /// Gets or sets the brush to use when drawing the button - /// - public string Placeholder - { - get => (string) GetValue(PlaceholderProperty); - set => SetValue(PlaceholderProperty, value); - } - - /// - /// Gets or sets the brush to use when drawing the button - /// - public bool ShowDataModelValues - { - get => (bool) GetValue(ShowDataModelValuesProperty); - set => SetValue(ShowDataModelValuesProperty, value); - } - - /// - /// Gets or sets the brush to use when drawing the button - /// - public bool ShowFullPath - { - get => (bool) GetValue(ShowFullPathProperty); - set => SetValue(ShowFullPathProperty, value); - } - - /// - /// Gets or sets the brush to use when drawing the button - /// - public Brush ButtonBrush - { - get => (Brush) GetValue(ButtonBrushProperty); - set => SetValue(ButtonBrushProperty, value); - } - - /// - /// Gets or sets a list of extra modules to show data models of - /// - public ObservableCollection? Modules - { - get => (ObservableCollection) GetValue(ModulesProperty); - set => SetValue(ModulesProperty, value); - } - - /// - /// Gets or sets the data model view model to show - /// - public DataModelPropertiesViewModel? DataModelViewModel - { - get => (DataModelPropertiesViewModel) GetValue(DataModelViewModelProperty); - set => SetValue(DataModelViewModelProperty, value); - } - - /// - /// Gets or sets a list of data model view models to show - /// - public ObservableCollection? ExtraDataModelViewModels - { - get => (ObservableCollection) GetValue(ExtraDataModelViewModelsProperty); - set => SetValue(ExtraDataModelViewModelsProperty, value); - } - - /// - /// Gets or sets the types of properties this view model will allow to be selected - /// - public ObservableCollection? FilterTypes - { - get => (ObservableCollection) GetValue(FilterTypesProperty); - set => SetValue(FilterTypesProperty, value); - } - - /// - /// Command used by view - /// - public DelegateCommand SelectPropertyCommand { get; } - - internal static IDataModelUIService DataModelUIService - { - set - { - if (_dataModelUIService != null) - throw new AccessViolationException("This is not for you to touch"); - _dataModelUIService = value; - } - } - - /// - /// Occurs when a new path has been selected - /// - public event EventHandler? DataModelPathSelected; - - /// - /// Invokes the event - /// - /// - protected virtual void OnDataModelPathSelected(DataModelSelectedEventArgs e) - { - DataModelPathSelected?.Invoke(this, e); - } - - private void GetDataModel() - { - ChangeDataModel(_dataModelUIService.GetPluginDataModelVisualization(Modules?.ToList() ?? new List(), true)); - } - - private void ChangeDataModel(DataModelPropertiesViewModel? dataModel) - { - if (DataModelViewModel != null) - { - DataModelViewModel.Dispose(); - DataModelViewModel.UpdateRequested -= DataModelOnUpdateRequested; - } - - DataModelViewModel = dataModel; - if (DataModelViewModel != null) - DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; - } - - private void UpdateValueDisplay() - { - ValueDisplay.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Visible : Visibility.Collapsed; - ValuePlaceholder.Visibility = DataModelPath == null || DataModelPath.IsValid ? Visibility.Collapsed : Visibility.Visible; - - string? formattedPath = null; - if (DataModelPath != null && DataModelPath.IsValid) - formattedPath = string.Join(" › ", DataModelPath.Segments.Where(s => s.GetPropertyDescription() != null).Select(s => s.GetPropertyDescription()!.Name)); - - DataModelButton.ToolTip = formattedPath; - ValueDisplayTextBlock.Text = ShowFullPath - ? formattedPath - : DataModelPath?.Segments.LastOrDefault()?.GetPropertyDescription()?.Name ?? DataModelPath?.Segments.LastOrDefault()?.Identifier; - } - - private void DataModelOnUpdateRequested(object? sender, EventArgs e) - { - DataModelViewModel?.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); - if (ExtraDataModelViewModels == null) return; - foreach (DataModelPropertiesViewModel extraDataModelViewModel in ExtraDataModelViewModels) - extraDataModelViewModel.ApplyTypeFilter(true, FilterTypes?.ToArray() ?? Type.EmptyTypes); - } - - private void ExecuteSelectPropertyCommand(object? context) - { - if (context is not DataModelVisualizationViewModel selected) - return; - - if (selected.DataModelPath == null) - return; - if (selected.DataModelPath.Equals(DataModelPath)) - return; - - DataModelPath = new DataModelPath(selected.DataModelPath); - OnDataModelPathSelected(new DataModelSelectedEventArgs(DataModelPath)); - } - - private void PropertyButton_OnClick(object sender, RoutedEventArgs e) - { - // DataContext is not set when using left button, I don't know why but there it is - if (sender is Button button && button.ContextMenu != null) - { - button.ContextMenu.DataContext = button.DataContext; - button.ContextMenu.IsOpen = true; - } - } - - private static void DataModelPathPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not DataModelPicker dataModelPicker) - return; - - if (e.OldValue is DataModelPath oldPath) - { - oldPath.PathInvalidated -= dataModelPicker.PathValidationChanged; - oldPath.PathValidated -= dataModelPicker.PathValidationChanged; - oldPath.Dispose(); - } - - dataModelPicker.UpdateValueDisplay(); - if (e.NewValue is DataModelPath newPath) - { - newPath.PathInvalidated += dataModelPicker.PathValidationChanged; - newPath.PathValidated += dataModelPicker.PathValidationChanged; - } - } - - private static void ShowFullPathPropertyCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not DataModelPicker dataModelPicker) - return; - - dataModelPicker.UpdateValueDisplay(); - } - - private static void ModulesPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not DataModelPicker dataModelPicker) - return; - - dataModelPicker.GetDataModel(); - } - - private static void DataModelViewModelPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not DataModelPicker dataModelPicker) - return; - - if (e.OldValue is DataModelPropertiesViewModel vm) - vm.Dispose(); - } - - private static void ExtraDataModelViewModelsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not DataModelPicker dataModelPicker) - return; - } - - private void PathValidationChanged(object? sender, EventArgs e) - { - Dispatcher.Invoke(UpdateValueDisplay, DispatcherPriority.DataBind); - } - - private void OnUnloaded(object o, RoutedEventArgs routedEventArgs) - { - if (DataModelPath != null) - { - DataModelPath.PathInvalidated -= PathValidationChanged; - DataModelPath.PathValidated -= PathValidationChanged; - } - - DataModelViewModel?.Dispose(); - } - - /// - public event PropertyChangedEventHandler? PropertyChanged; - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs b/src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs rename to src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs b/src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs rename to src/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPickerButton.cs diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index 427ad1808..b5a944faf 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -1,94 +1,93 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.ComponentModel; using System.IO; using System.Linq; -using System.Timers; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Threading; +using System.Threading.Tasks; using Artemis.Core; +using Artemis.UI.Shared.Events; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; +using Avalonia.Visuals.Media.Imaging; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Controls { /// /// Visualizes an with optional per-LED colors /// - public class DeviceVisualizer : FrameworkElement + public class DeviceVisualizer : Control { - /// - /// The device to visualize - /// - public static readonly DependencyProperty DeviceProperty = DependencyProperty.Register(nameof(Device), typeof(ArtemisDevice), typeof(DeviceVisualizer), - new FrameworkPropertyMetadata(default(ArtemisDevice), FrameworkPropertyMetadataOptions.AffectsRender, DevicePropertyChangedCallback)); - - /// - /// Whether or not to show per-LED colors - /// - public static readonly DependencyProperty ShowColorsProperty = DependencyProperty.Register(nameof(ShowColors), typeof(bool), typeof(DeviceVisualizer), - new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender, ShowColorsPropertyChangedCallback)); - - /// - /// A list of LEDs to highlight - /// - public static readonly DependencyProperty HighlightedLedsProperty = DependencyProperty.Register(nameof(HighlightedLeds), typeof(ObservableCollection), typeof(DeviceVisualizer), - new FrameworkPropertyMetadata(default(ObservableCollection), HighlightedLedsPropertyChanged)); - - private readonly DrawingGroup _backingStore; + private const double UpdateFrameRate = 25.0; private readonly List _deviceVisualizerLeds; private readonly DispatcherTimer _timer; - private BitmapImage? _deviceImage; - private ArtemisDevice? _oldDevice; - private List _highlightedLeds; - private List _dimmedLeds; - /// - /// Creates a new instance of the class - /// + private Rect _deviceBounds; + private RenderTargetBitmap? _deviceImage; + private List? _dimmedLeds; + private List? _highlightedLeds; + private ArtemisDevice? _oldDevice; + + /// public DeviceVisualizer() { - _backingStore = new DrawingGroup(); + _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(1000.0 / UpdateFrameRate)}; _deviceVisualizerLeds = new List(); - _dimmedLeds = new List(); - // Run an update timer at 25 fps - _timer = new DispatcherTimer(DispatcherPriority.Render) {Interval = TimeSpan.FromMilliseconds(40)}; - - MouseLeftButtonUp += OnMouseLeftButtonUp; - Loaded += OnLoaded; - Unloaded += OnUnloaded; + PointerReleased += OnPointerReleased; } - /// - /// Gets or sets the device to visualize - /// - public ArtemisDevice? Device + /// + public override void Render(DrawingContext drawingContext) { - get => (ArtemisDevice) GetValue(DeviceProperty); - set => SetValue(DeviceProperty, value); - } + if (Device == null) + return; - /// - /// Gets or sets whether or not to show per-LED colors - /// - public bool ShowColors - { - get => (bool) GetValue(ShowColorsProperty); - set => SetValue(ShowColorsProperty, value); - } + // Determine the scale required to fit the desired size of the control + double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height); - /// - /// Gets or sets a list of LEDs to highlight - /// - public ObservableCollection? HighlightedLeds - { - get => (ObservableCollection) GetValue(HighlightedLedsProperty); - set => SetValue(HighlightedLedsProperty, value); + DrawingContext.PushedState? boundsPush = null; + try + { + // Scale the visualization in the desired bounding box + if (Bounds.Width > 0 && Bounds.Height > 0) + boundsPush = drawingContext.PushPreTransform(Matrix.CreateScale(scale, scale)); + + // Apply device rotation + using DrawingContext.PushedState translationPush = drawingContext.PushPreTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top)); + using DrawingContext.PushedState rotationPush = drawingContext.PushPreTransform(Matrix.CreateRotation(Matrix.ToRadians(Device.Rotation))); + + // Apply device scale + using DrawingContext.PushedState scalePush = drawingContext.PushPreTransform(Matrix.CreateScale(Device.Scale, Device.Scale)); + + // Render device and LED images + if (_deviceImage != null) + { + drawingContext.DrawImage( + _deviceImage, + new Rect(_deviceImage.Size), + new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height), + RenderOptions.GetBitmapInterpolationMode(this) + ); + } + + if (!ShowColors) + return; + + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.RenderGeometry(drawingContext, false); + } + finally + { + boundsPush?.Dispose(); + } } /// @@ -96,54 +95,6 @@ namespace Artemis.UI.Shared /// public event EventHandler? LedClicked; - /// - protected override void OnRender(DrawingContext drawingContext) - { - if (Device == null) - return; - - // Determine the scale required to fit the desired size of the control - Size measureSize = MeasureDevice(); - double scale = Math.Min(RenderSize.Width / measureSize.Width, RenderSize.Height / measureSize.Height); - - // Scale the visualization in the desired bounding box - if (RenderSize.Width > 0 && RenderSize.Height > 0) - drawingContext.PushTransform(new ScaleTransform(scale, scale)); - - // Determine the offset required to rotate within bounds - Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height); - rotationRect.Transform(new RotateTransform(Device.Rotation).Value); - - // Apply device rotation - drawingContext.PushTransform(new TranslateTransform(0 - rotationRect.Left, 0 - rotationRect.Top)); - drawingContext.PushTransform(new RotateTransform(Device.Rotation)); - - // Apply device scale - drawingContext.PushTransform(new ScaleTransform(Device.Scale, Device.Scale)); - - // Render device and LED images - if (_deviceImage != null) - drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.Size.Width, Device.RgbDevice.Size.Height)); - - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderImage(drawingContext); - - drawingContext.DrawDrawing(_backingStore); - } - - /// - protected override Size MeasureOverride(Size availableSize) - { - if (Device == null) - return Size.Empty; - - Size deviceSize = MeasureDevice(); - if (deviceSize.Width <= 0 || deviceSize.Height <= 0) - return Size.Empty; - - return ResizeKeepAspect(deviceSize, availableSize.Width, availableSize.Height); - } - /// /// Invokes the event /// @@ -153,72 +104,37 @@ namespace Artemis.UI.Shared LedClicked?.Invoke(this, e); } - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) + private void Update() { - if (disposing) - _timer.Stop(); + InvalidateVisual(); } - private static Size ResizeKeepAspect(Size src, double maxWidth, double maxHeight) - { - double scale; - if (double.IsPositiveInfinity(maxWidth) && !double.IsPositiveInfinity(maxHeight)) - scale = maxHeight / src.Height; - else if (!double.IsPositiveInfinity(maxWidth) && double.IsPositiveInfinity(maxHeight)) - scale = maxWidth / src.Width; - else if (double.IsPositiveInfinity(maxWidth) && double.IsPositiveInfinity(maxHeight)) - return src; - else - scale = Math.Min(maxWidth / src.Width, maxHeight / src.Height); - - return new Size(src.Width * scale, src.Height * scale); - } - - private Size MeasureDevice() + private Rect MeasureDevice() { if (Device == null) - return Size.Empty; + return Rect.Empty; - Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height); - rotationRect.Transform(new RotateTransform(Device.Rotation).Value); + Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height); + Geometry geometry = new RectangleGeometry(deviceRect); + geometry.Transform = new RotateTransform(Device.Rotation); - return rotationRect.Size; + return geometry.Bounds; } - private void OnUnloaded(object? sender, RoutedEventArgs e) + private void TimerOnTick(object? sender, EventArgs e) { - _timer.Stop(); - _timer.Tick -= TimerOnTick; - - if (HighlightedLeds != null) - HighlightedLeds.CollectionChanged -= HighlightedLedsChanged; - if (_oldDevice != null) - { - if (Device != null) - { - Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; - Device.DeviceUpdated -= DeviceUpdated; - } - - _oldDevice = null; - } + if (ShowColors && IsVisible && Opacity > 0) + Update(); } - private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { if (Device == null) return; Point position = e.GetPosition(this); - double x = position.X / RenderSize.Width; - double y = position.Y / RenderSize.Height; + double x = position.X / Bounds.Width; + double y = position.Y / Bounds.Height; Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height); DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.HitTest(scaledPosition)); @@ -226,154 +142,173 @@ namespace Artemis.UI.Shared OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led)); } - private void OnLoaded(object? sender, RoutedEventArgs e) + private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) { - _timer.Start(); - _timer.Tick += TimerOnTick; + Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background); } - private void TimerOnTick(object? sender, EventArgs e) + private void DeviceUpdated(object? sender, EventArgs e) { - if (ShowColors && Visibility == Visibility.Visible) - Render(); + Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background); } - private void Render() + #region Properties + + /// + /// Gets or sets the to display + /// + public static readonly StyledProperty DeviceProperty = + AvaloniaProperty.Register(nameof(Device), notifying: DeviceUpdated); + + private static void DeviceUpdated(IAvaloniaObject sender, bool before) { - DrawingContext drawingContext = _backingStore.Append(); - - if (_highlightedLeds.Any()) - { - foreach (DeviceVisualizerLed deviceVisualizerLed in _highlightedLeds) - deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false); - - foreach (DeviceVisualizerLed deviceVisualizerLed in _dimmedLeds) - deviceVisualizerLed.RenderColor(_backingStore, drawingContext, true); - } - else - { - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderColor(_backingStore, drawingContext, false); - } - - drawingContext.Close(); + if (!before) + ((DeviceVisualizer) sender).SetupForDevice(); } - private void UpdateTransform() + /// + /// Gets or sets the to display + /// + public ArtemisDevice? Device { - InvalidateVisual(); - InvalidateMeasure(); + get => GetValue(DeviceProperty); + set => SetValue(DeviceProperty, value); } - private void SetupForDevice() + /// + /// Gets or sets boolean indicating whether or not to show per-LED colors + /// + public static readonly StyledProperty ShowColorsProperty = + AvaloniaProperty.Register(nameof(ShowColors)); + + /// + /// Gets or sets a boolean indicating whether or not to show per-LED colors + /// + public bool ShowColors { + get => GetValue(ShowColorsProperty); + set => SetValue(ShowColorsProperty, value); + } + + /// + /// Gets or sets a list of LEDs to highlight + /// + public static readonly StyledProperty?> HighlightedLedsProperty = + AvaloniaProperty.Register?>(nameof(HighlightedLeds)); + + /// + /// Gets or sets a list of LEDs to highlight + /// + public ObservableCollection? HighlightedLeds + { + get => GetValue(HighlightedLedsProperty); + set => SetValue(HighlightedLedsProperty, value); + } + + #endregion + + #region Lifetime management + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _deviceImage?.Dispose(); _deviceImage = null; - _deviceVisualizerLeds.Clear(); - _highlightedLeds = new List(); - _dimmedLeds = new List(); - if (Device == null) - return; - - if (_oldDevice != null) + if (Device != null) { Device.RgbDevice.PropertyChanged -= DevicePropertyChanged; Device.DeviceUpdated -= DeviceUpdated; } + base.OnDetachedFromVisualTree(e); + } + + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _timer.Start(); + _timer.Tick += TimerOnTick; + base.OnAttachedToLogicalTree(e); + } + + /// + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _timer.Stop(); + _timer.Tick -= TimerOnTick; + base.OnDetachedFromLogicalTree(e); + } + + private void SetupForDevice() + { + _deviceImage?.Dispose(); + _deviceImage = null; + _deviceVisualizerLeds.Clear(); + _highlightedLeds = new List(); + _dimmedLeds = new List(); + + if (_oldDevice != null) + { + _oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged; + _oldDevice.DeviceUpdated -= DeviceUpdated; + } + _oldDevice = Device; + if (Device == null) + return; + + _deviceBounds = MeasureDevice(); Device.RgbDevice.PropertyChanged += DevicePropertyChanged; Device.DeviceUpdated += DeviceUpdated; - UpdateTransform(); - - // Load the device main image - if (Device.Layout?.Image != null && File.Exists(Device.Layout.Image.LocalPath)) - _deviceImage = new BitmapImage(Device.Layout.Image); // Create all the LEDs foreach (ArtemisLed artemisLed in Device.Leds) _deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed)); - if (!ShowColors) + // Load the device main image on a background thread + ArtemisDevice? device = Device; + Task.Run(() => { - InvalidateMeasure(); - return; - } + if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath)) + return; - // Create the opacity drawing group - DrawingGroup opacityDrawingGroup = new(); - DrawingContext drawingContext = opacityDrawingGroup.Open(); - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderOpacityMask(drawingContext); - drawingContext.Close(); + try + { + // Create a bitmap that'll be used to render the device and LED images just once + RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.Size.Width * 4, (int) device.RgbDevice.Size.Height * 4)); - // Render the store as a bitmap - DrawingImage drawingImage = new(opacityDrawingGroup); - Image image = new() {Source = drawingImage}; - RenderTargetBitmap bitmap = new( - Math.Max(1, (int) (opacityDrawingGroup.Bounds.Width * 2.5)), - Math.Max(1, (int) (opacityDrawingGroup.Bounds.Height * 2.5)), - 96, - 96, - PixelFormats.Pbgra32 - ); - image.Arrange(new Rect(0, 0, bitmap.Width, bitmap.Height)); - bitmap.Render(image); - bitmap.Freeze(); + using IDrawingContextImpl context = renderTargetBitmap.CreateDrawingContext(new ImmediateRenderer(this)); + using Bitmap bitmap = new(device.Layout.Image.LocalPath); + context.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(renderTargetBitmap.Size), BitmapInterpolationMode.HighQuality); + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.DrawBitmap(context); - // Set the bitmap as the opacity mask for the colors backing store - ImageBrush bitmapBrush = new(bitmap); - bitmapBrush.Freeze(); - _backingStore.OpacityMask = bitmapBrush; - _backingStore.Children.Clear(); + _deviceImage = renderTargetBitmap; - InvalidateMeasure(); + Dispatcher.UIThread.Post(InvalidateMeasure); + } + catch + { + // ignored + } + }); } - private void DeviceUpdated(object? sender, EventArgs e) + /// + protected override Size MeasureOverride(Size availableSize) { - Dispatcher.Invoke(SetupForDevice); + if (_deviceBounds.Width <= 0 || _deviceBounds.Height <= 0) + return new Size(0, 0); + + double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width; + double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height; + double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height); + + return new Size(_deviceBounds.Width * bestRatio, _deviceBounds.Height * bestRatio); } - private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e) - { - Dispatcher.Invoke(SetupForDevice); - } - - private static void DevicePropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; - deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); }); - } - - private static void ShowColorsPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; - deviceVisualizer.Dispatcher.Invoke(() => { deviceVisualizer.SetupForDevice(); }); - } - - private static void HighlightedLedsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - DeviceVisualizer deviceVisualizer = (DeviceVisualizer) d; - if (e.OldValue is ObservableCollection oldCollection) - oldCollection.CollectionChanged -= deviceVisualizer.HighlightedLedsChanged; - if (e.NewValue is ObservableCollection newCollection) - newCollection.CollectionChanged += deviceVisualizer.HighlightedLedsChanged; - } - - private void HighlightedLedsChanged(object? sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) - { - if (HighlightedLeds != null) - { - _highlightedLeds = _deviceVisualizerLeds.Where(l => HighlightedLeds.Contains(l.Led)).ToList(); - _dimmedLeds = _deviceVisualizerLeds.Where(l => !HighlightedLeds.Contains(l.Led)).ToList(); - } - else - { - _highlightedLeds = new List(); - _dimmedLeds = new List(); - } - } + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index b6cdae397..9ccc4bcb6 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -1,24 +1,23 @@ using System; using System.IO; -using System.Windows; -using System.Windows.Media; -using System.Windows.Media.Imaging; using Artemis.Core; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; using RGB.NET.Core; -using Color = System.Windows.Media.Color; -using Point = System.Windows.Point; -using SolidColorBrush = System.Windows.Media.SolidColorBrush; +using Color = Avalonia.Media.Color; +using Point = Avalonia.Point; +using SolidColorBrush = Avalonia.Media.SolidColorBrush; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Controls { internal class DeviceVisualizerLed { - private const byte Dimmed = 100; - private const byte NonDimmed = 255; - private GeometryDrawing? _geometryDrawing; - private DrawingGroup? _lastBackingStore; - private Color _renderColor; - private SolidColorBrush? _renderColorBrush; + private readonly SolidColorBrush _penBrush; + private readonly SolidColorBrush _fillBrush; + private readonly Pen _pen; public DeviceVisualizerLed(ArtemisLed led) { @@ -30,83 +29,63 @@ namespace Artemis.UI.Shared Led.RgbLed.Size.Height ); - if (Led.Layout?.Image != null && File.Exists(Led.Layout.Image.LocalPath)) - LedImage = new BitmapImage(Led.Layout.Image); + _fillBrush = new SolidColorBrush(); + _penBrush = new SolidColorBrush(); + _pen = new Pen(_penBrush) {LineJoin = PenLineJoin.Round}; CreateLedGeometry(); } - public ArtemisLed Led { get; } public Rect LedRect { get; set; } - public BitmapImage? LedImage { get; set; } public Geometry? DisplayGeometry { get; private set; } - public void RenderColor(DrawingGroup backingStore, DrawingContext drawingContext, bool isDimmed) + public void DrawBitmap(IDrawingContextImpl drawingContext) { - if (DisplayGeometry == null || backingStore == null) + if (Led.Layout?.Image == null || !File.Exists(Led.Layout.Image.LocalPath)) return; - _renderColorBrush ??= new SolidColorBrush(); - _geometryDrawing ??= new GeometryDrawing(_renderColorBrush, null, new RectangleGeometry(LedRect)); + try + { + using Bitmap bitmap = new(Led.Layout.Image.LocalPath); + drawingContext.DrawBitmap( + bitmap.PlatformImpl, + 1, + new Rect(bitmap.Size), + new Rect(Led.RgbLed.Location.X * 4, Led.RgbLed.Location.Y * 4, Led.RgbLed.Size.Width * 4, Led.RgbLed.Size.Height * 4), + BitmapInterpolationMode.HighQuality + ); + } + catch + { + // ignored + } + } + public void RenderGeometry(DrawingContext drawingContext, bool dimmed) + { byte r = Led.RgbLed.Color.GetR(); byte g = Led.RgbLed.Color.GetG(); byte b = Led.RgbLed.Color.GetB(); - _renderColor.A = isDimmed ? Dimmed : NonDimmed; - _renderColor.R = r; - _renderColor.G = g; - _renderColor.B = b; - _renderColorBrush.Color = _renderColor; - - if (_lastBackingStore != backingStore) + if (dimmed) { - backingStore.Children.Add(_geometryDrawing); - _lastBackingStore = backingStore; + _fillBrush.Color = new Color(50, r, g, b); + _penBrush.Color = new Color(100, r, g, b); + } + else + { + _fillBrush.Color = new Color(100, r, g, b); + _penBrush.Color = new Color(255, r, g, b); } - } - public void RenderImage(DrawingContext drawingContext) - { - if (LedImage == null) - return; - - drawingContext.DrawImage(LedImage, LedRect); - } - - public void RenderOpacityMask(DrawingContext drawingContext) - { - if (DisplayGeometry == null) - return; - - SolidColorBrush fillBrush = new(Color.FromArgb(100, 255, 255, 255)); - fillBrush.Freeze(); - SolidColorBrush penBrush = new(Color.FromArgb(255, 255, 255, 255)); - penBrush.Freeze(); - - // Create transparent pixels covering the entire LedRect so the image size matched the LedRect size - drawingContext.DrawRectangle(new SolidColorBrush(Colors.Transparent), new Pen(new SolidColorBrush(Colors.Transparent), 1), LedRect); - // Translate to the top-left of the LedRect - drawingContext.PushTransform(new TranslateTransform(LedRect.X, LedRect.Y)); // Render the LED geometry - drawingContext.DrawGeometry(fillBrush, new Pen(penBrush, 1) {LineJoin = PenLineJoin.Round}, DisplayGeometry.GetOutlinedPathGeometry()); - // Restore the drawing context - drawingContext.Pop(); + drawingContext.DrawGeometry(_fillBrush, _pen, DisplayGeometry); } public bool HitTest(Point position) { - if (DisplayGeometry == null) - return false; - - PathGeometry translatedGeometry = Geometry.Combine( - Geometry.Empty, - DisplayGeometry, - GeometryCombineMode.Union, - new TranslateTransform(Led.RgbLed.Location.X, Led.RgbLed.Location.Y) - ); - return translatedGeometry.FillContains(position); + return DisplayGeometry != null && DisplayGeometry.FillContains(position); } private void CreateLedGeometry() @@ -139,17 +118,17 @@ namespace Artemis.UI.Shared private void CreateRectangleGeometry() { - DisplayGeometry = new RectangleGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); + DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); } private void CreateCircleGeometry() { - DisplayGeometry = new EllipseGeometry(new Rect(0.5, 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); + DisplayGeometry = new EllipseGeometry(new Rect(Led.RgbLed.Location.X + 0.5, Led.RgbLed.Location.Y + 0.5, Led.RgbLed.Size.Width - 1, Led.RgbLed.Size.Height - 1)); } private void CreateKeyCapGeometry() { - DisplayGeometry = new RectangleGeometry(new Rect(1, 1, Led.RgbLed.Size.Width - 2, Led.RgbLed.Size.Height - 2), 1.6, 1.6); + DisplayGeometry = new RectangleGeometry(new Rect(Led.RgbLed.Location.X + 1, Led.RgbLed.Location.Y + 1, Led.RgbLed.Size.Width - 2, Led.RgbLed.Size.Height - 2)); } private void CreateCustomGeometry(double deflateAmount) @@ -158,36 +137,17 @@ namespace Artemis.UI.Shared { double width = Led.RgbLed.Size.Width - deflateAmount; double height = Led.RgbLed.Size.Height - deflateAmount; - // DisplayGeometry = Geometry.Parse(Led.RgbLed.ShapeData); - DisplayGeometry = Geometry.Combine( - Geometry.Empty, - Geometry.Parse(Led.RgbLed.ShapeData), - GeometryCombineMode.Union, - new TransformGroup + + Geometry geometry = Geometry.Parse(Led.RgbLed.ShapeData); + geometry.Transform = new TransformGroup + { + Children = new Transforms { - Children = new TransformCollection - { - new ScaleTransform(width, height), - new TranslateTransform(deflateAmount / 2, deflateAmount / 2) - } + new ScaleTransform(width, height), + new TranslateTransform(Led.RgbLed.Location.X + deflateAmount / 2, Led.RgbLed.Location.Y + deflateAmount / 2) } - ); - - if (DisplayGeometry.Bounds.Width > width) - { - DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup - { - Children = new TransformCollection {new ScaleTransform(width / DisplayGeometry.Bounds.Width, 1)} - }); - } - - if (DisplayGeometry.Bounds.Height > height) - { - DisplayGeometry = Geometry.Combine(Geometry.Empty, DisplayGeometry, GeometryCombineMode.Union, new TransformGroup - { - Children = new TransformCollection {new ScaleTransform(1, height / DisplayGeometry.Bounds.Height)} - }); - } + }; + DisplayGeometry = geometry; } catch (Exception) { diff --git a/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml b/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml deleted file mode 100644 index e7dcffe2d..000000000 --- a/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml.cs b/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml.cs deleted file mode 100644 index 499afb5d7..000000000 --- a/src/Artemis.UI.Shared/Controls/DraggableFloat.xaml.cs +++ /dev/null @@ -1,303 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; -using System.Windows; -using System.Windows.Input; - -namespace Artemis.UI.Shared -{ - /// - /// Interaction logic for DraggableFloat.xaml - /// - public partial class DraggableFloat : INotifyPropertyChanged - { - /// - /// Gets or sets the current value - /// - public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(float), typeof(DraggableFloat), - new FrameworkPropertyMetadata(default(float), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, FloatPropertyChangedCallback)); - - /// - /// Gets or sets the step size when dragging - /// - public static readonly DependencyProperty StepSizeProperty = DependencyProperty.Register(nameof(StepSize), typeof(float), typeof(DraggableFloat)); - - /// - /// Gets or sets the minimum value - /// - public static readonly DependencyProperty MinProperty = DependencyProperty.Register(nameof(Min), typeof(object), typeof(DraggableFloat)); - - /// - /// Gets or sets the maximum value - /// - public static readonly DependencyProperty MaxProperty = DependencyProperty.Register(nameof(Max), typeof(object), typeof(DraggableFloat)); - - /// - /// Occurs when the value has changed - /// - public static readonly RoutedEvent ValueChangedEvent = - EventManager.RegisterRoutedEvent( - nameof(Value), - RoutingStrategy.Bubble, - typeof(RoutedPropertyChangedEventHandler), - typeof(DraggableFloat)); - - private readonly Regex _inputRegex = new("^[.][-|0-9]+$|^-?[0-9]*[.]{0,1}[0-9]*$"); - - private bool _calledDragStarted; - - private bool _inCallback; - private Point _mouseDragStartPoint; - private float _startValue; - - /// - /// Creates a new instance of the class - /// - public DraggableFloat() - { - InitializeComponent(); - } - - /// - /// Gets or sets the current value - /// - public float Value - { - get => (float) GetValue(ValueProperty); - set => SetValue(ValueProperty, value); - } - - /// - /// Gets or sets the current value as a string - /// - public string InputValue - { - get => Value.ToString("N3", CultureInfo.InvariantCulture); - set => UpdateValue(value); - } - - /// - /// Gets or sets the step size when dragging - /// - public float StepSize - { - get => (float) GetValue(StepSizeProperty); - set => SetValue(StepSizeProperty, value); - } - - /// - /// Gets or sets the minimum value - /// - public object? Min - { - get => (object?) GetValue(MinProperty); - set => SetValue(MinProperty, value); - } - - /// - /// Gets or sets the maximum value - /// - public object? Max - { - get => (object?) GetValue(MaxProperty); - set => SetValue(MaxProperty, value); - } - - private void UpdateValue(string value) - { - if (!float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedResult)) - return; - - Value = parsedResult; - OnPropertyChanged(nameof(InputValue)); - } - - - private void DisplayInput() - { - DragHandle.Visibility = Visibility.Collapsed; - DraggableFloatInputTextBox.Visibility = Visibility.Visible; - DraggableFloatInputTextBox.Focus(); - DraggableFloatInputTextBox.SelectAll(); - } - - private void DisplayDragHandle() - { - DraggableFloatInputTextBox.Visibility = Visibility.Collapsed; - DragHandle.Visibility = Visibility.Visible; - } - - - /// - /// Rounds the provided decimal to the nearest value of x with a given threshold - /// Source: https://stackoverflow.com/a/25922075/5015269 - /// - /// The value to round - /// The value to round down towards - private static decimal RoundToNearestOf(decimal input, decimal nearestOf) - { - return Math.Floor(input / nearestOf + 0.5m) * nearestOf; - } - - #region Event handlers - - private static void FloatPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - DraggableFloat draggableFloat = (DraggableFloat) d; - if (draggableFloat._inCallback) - return; - - draggableFloat._inCallback = true; - draggableFloat.OnPropertyChanged(nameof(Value)); - draggableFloat.OnPropertyChanged(nameof(InputValue)); - draggableFloat._inCallback = false; - } - - private void InputMouseDown(object sender, MouseButtonEventArgs e) - { - e.Handled = true; - - _startValue = Value; - ((IInputElement) sender).CaptureMouse(); - _mouseDragStartPoint = e.GetPosition((IInputElement) sender); - } - - private void InputMouseUp(object sender, MouseButtonEventArgs e) - { - e.Handled = true; - - Point position = e.GetPosition((IInputElement) sender); - if (position == _mouseDragStartPoint) - { - DisplayInput(); - } - else - { - OnDragEnded(); - _calledDragStarted = false; - } - - ((IInputElement) sender).ReleaseMouseCapture(); - } - - private void InputMouseMove(object sender, MouseEventArgs e) - { - if (e.LeftButton != MouseButtonState.Pressed) - return; - - e.Handled = true; - - if (!_calledDragStarted) - { - OnDragStarted(); - _calledDragStarted = true; - } - - // Use decimals for everything to avoid floating point errors - decimal startValue = new(_startValue); - decimal startX = new(_mouseDragStartPoint.X); - decimal x = new(e.GetPosition((IInputElement) sender).X); - decimal stepSize = new(StepSize); - if (stepSize == 0) - stepSize = 0.1m; - - if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) - stepSize = stepSize * 10; - - float value = (float) RoundToNearestOf(startValue + stepSize * (x - startX), stepSize); - - if (Min != null && float.TryParse(Min.ToString(), out float minFloat)) - value = Math.Max(value, minFloat); - if (Max != null && float.TryParse(Max.ToString(), out float maxFloat)) - value = Math.Min(value, maxFloat); - - Value = value; - } - - private void InputLostFocus(object sender, RoutedEventArgs e) - { - DisplayDragHandle(); - } - - private void InputKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - DisplayDragHandle(); - } - else if (e.Key == Key.Escape) - { - DraggableFloatInputTextBox.Text = _startValue.ToString(CultureInfo.InvariantCulture); - DisplayDragHandle(); - } - } - - private void Input_OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) - { - e.Handled = true; - } - - private void Input_PreviewTextInput(object sender, TextCompositionEventArgs e) - { - e.Handled = !_inputRegex.IsMatch(e.Text); - } - - private void Input_OnPasting(object sender, DataObjectPastingEventArgs e) - { - if (e.DataObject.GetDataPresent(typeof(string))) - { - if (e.DataObject.GetData(typeof(string)) is string text && !_inputRegex.IsMatch(text)) - e.CancelCommand(); - } - else - { - e.CancelCommand(); - } - } - - #endregion - - #region Events - - /// - public event PropertyChangedEventHandler? PropertyChanged; - - /// - /// Occurs when dragging has started - /// - public event EventHandler? DragStarted; - - /// - /// Occurs when dragging has ended - /// - public event EventHandler? DragEnded; - - /// - /// Invokes the event - /// - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDragStarted() - { - DragStarted?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the event - /// - protected virtual void OnDragEnded() - { - DragEnded?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml b/src/Artemis.UI.Shared/Controls/EnumComboBox.axaml similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml rename to src/Artemis.UI.Shared/Controls/EnumComboBox.axaml diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs b/src/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs rename to src/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs b/src/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs rename to src/Artemis.UI.Shared/Controls/Flyouts/DataModelPickerFlyout.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs b/src/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs rename to src/Artemis.UI.Shared/Controls/Flyouts/GradientPickerFlyout.cs diff --git a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml deleted file mode 100644 index c13770ec8..000000000 --- a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs deleted file mode 100644 index aaedaa6bc..000000000 --- a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Windows; -using System.Windows.Input; -using System.Windows.Media; -using Artemis.Core; -using Artemis.UI.Shared.Properties; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Shared -{ - /// - /// Interaction logic for GradientPicker.xaml - /// - public partial class GradientPicker : INotifyPropertyChanged - { - private static IColorPickerService? _colorPickerService; - private bool _inCallback; - private ColorGradientToGradientStopsConverter _gradientConverter; - - /// - /// Creates a new instance of the class - /// - public GradientPicker() - { - _gradientConverter = new ColorGradientToGradientStopsConverter(); - InitializeComponent(); - Unloaded += OnUnloaded; - } - - /// - /// Gets or sets the color gradient - /// - public ColorGradient ColorGradient - { - get => (ColorGradient) GetValue(ColorGradientProperty); - set => SetValue(ColorGradientProperty, value); - } - - /// - /// Gets or sets the dialog host in which to show the gradient dialog - /// - public string DialogHost - { - get => (string) GetValue(DialogHostProperty); - set => SetValue(DialogHostProperty, value); - } - - /// - /// Used by the gradient picker to load saved gradients, do not touch or it'll just throw an exception - /// - internal static IColorPickerService ColorPickerService - { - set - { - if (_colorPickerService != null) - throw new AccessViolationException("This is not for you to touch"); - _colorPickerService = value; - } - } - - /// - /// Occurs when the dialog has opened - /// - public event EventHandler? DialogOpened; - - /// - /// Occurs when the dialog has closed - /// - public event EventHandler? DialogClosed; - - /// - /// Invokes the event - /// - [NotifyPropertyChangedInvocator] - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDialogOpened() - { - DialogOpened?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the event - /// - protected virtual void OnDialogClosed() - { - DialogClosed?.Invoke(this, EventArgs.Empty); - } - - private static void ColorGradientPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - GradientPicker gradientPicker = (GradientPicker) d; - if (gradientPicker._inCallback) - return; - - gradientPicker._inCallback = true; - - if (e.OldValue is ColorGradient oldGradient) - oldGradient.CollectionChanged -= gradientPicker.GradientChanged; - if (e.NewValue is ColorGradient newGradient) - newGradient.CollectionChanged += gradientPicker.GradientChanged; - gradientPicker.UpdateGradientStops(); - gradientPicker.OnPropertyChanged(nameof(ColorGradient)); - - gradientPicker._inCallback = false; - } - - private void GradientChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - Dispatcher.Invoke(UpdateGradientStops); - } - - private void UpdateGradientStops() - { - GradientPreview.GradientStops = (GradientStopCollection) _gradientConverter.Convert(ColorGradient, null!, null!, null!); - } - - private void UIElement_OnMouseUp(object sender, MouseButtonEventArgs e) - { - if (_colorPickerService == null) - return; - - Execute.OnUIThread(async () => - { - OnDialogOpened(); - await _colorPickerService.ShowGradientPicker(ColorGradient, DialogHost); - OnDialogClosed(); - }); - } - - private void OnUnloaded(object sender, RoutedEventArgs e) - { - ColorGradient.CollectionChanged -= GradientChanged; - } - - /// - public event PropertyChangedEventHandler? PropertyChanged; - - #region Static WPF fields - - /// - /// Gets or sets the color gradient - /// - public static readonly DependencyProperty ColorGradientProperty = DependencyProperty.Register(nameof(ColorGradient), typeof(ColorGradient), typeof(GradientPicker), - new FrameworkPropertyMetadata(default(ColorGradient), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ColorGradientPropertyChangedCallback)); - - /// - /// Gets or sets the dialog host in which to show the gradient dialog - /// - public static readonly DependencyProperty DialogHostProperty = DependencyProperty.Register(nameof(DialogHost), typeof(string), typeof(GradientPicker), - new FrameworkPropertyMetadata(default(string))); - - /// - /// Occurs when the color gradient has changed - /// - public static readonly RoutedEvent ColorGradientChangedEvent = - EventManager.RegisterRoutedEvent(nameof(ColorGradient), RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(GradientPicker)); - - #endregion - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs rename to src/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerButton.cs b/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerButton.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerButton.cs rename to src/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerButton.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs b/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs rename to src/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml b/src/Artemis.UI.Shared/Controls/HotkeyBox.axaml similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml rename to src/Artemis.UI.Shared/Controls/HotkeyBox.axaml diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs b/src/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs rename to src/Artemis.UI.Shared/Controls/HotkeyBox.axaml.cs diff --git a/src/Artemis.UI.Shared/Controls/LockableToggleButton.cs b/src/Artemis.UI.Shared/Controls/LockableToggleButton.cs deleted file mode 100644 index 52748cc0e..000000000 --- a/src/Artemis.UI.Shared/Controls/LockableToggleButton.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Windows; -using System.Windows.Controls.Primitives; - -namespace Artemis.UI.Shared -{ - /// - /// Represents a toggle button that can be locked using a property - /// - public class LockableToggleButton : ToggleButton - { - /// - /// Gets or sets a boolean indicating whether the toggle button is locked - /// - public static readonly DependencyProperty IsLockedProperty = - DependencyProperty.Register("IsLocked", typeof(bool), typeof(LockableToggleButton), new UIPropertyMetadata(false)); - - /// - /// Gets or sets a boolean indicating whether the toggle button is locked - /// - public bool IsLocked - { - get => (bool) GetValue(IsLockedProperty); - set => SetValue(IsLockedProperty, value); - } - - /// - protected override void OnToggle() - { - if (!IsLocked) base.OnToggle(); - } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/NoInputTextBox.cs b/src/Artemis.UI.Shared/Controls/NoInputTextBox.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/NoInputTextBox.cs rename to src/Artemis.UI.Shared/Controls/NoInputTextBox.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml rename to src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs rename to src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml deleted file mode 100644 index 1572fc3d9..000000000 --- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs deleted file mode 100644 index d63317f28..000000000 --- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Windows; -using System.Windows.Controls; - -namespace Artemis.UI.Shared - -{ - /// - /// Interaction logic for ProfileConfigurationIcon.xaml - /// - public partial class ProfileConfigurationIcon : UserControl - { - /// - /// Gets or sets the to display - /// - public static readonly DependencyProperty ConfigurationIconProperty = - DependencyProperty.Register(nameof(ConfigurationIcon), typeof(Core.ProfileConfigurationIcon), typeof(ProfileConfigurationIcon)); - - /// - /// Creates a new instance of the class - /// - public ProfileConfigurationIcon() - { - InitializeComponent(); - } - - /// - /// Gets or sets the to display - /// - public Core.ProfileConfigurationIcon ConfigurationIcon - { - get => (Core.ProfileConfigurationIcon) GetValue(ConfigurationIconProperty); - set => SetValue(ConfigurationIconProperty, value); - } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Artemis.UI.Shared/Controls/SelectionRectangle.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs rename to src/Artemis.UI.Shared/Controls/SelectionRectangle.cs diff --git a/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs b/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs index 2a1c9de0a..29f562d47 100644 --- a/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs +++ b/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs @@ -1,46 +1,41 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Windows.Data; -using System.Windows.Media; using Artemis.Core; +using Avalonia.Data.Converters; +using Avalonia.Media; using SkiaSharp; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Converters; + +/// +/// Converts into a . +/// +public class ColorGradientToGradientStopsConverter : IValueConverter { /// - /// - /// Converts into a - /// . - /// - [ValueConversion(typeof(ColorGradient), typeof(GradientStopCollection))] - public class ColorGradientToGradientStopsConverter : IValueConverter + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - ColorGradient? colorGradient = value as ColorGradient; - GradientStopCollection collection = new(); - if (colorGradient == null) - return collection; - - foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position)) - collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position)); + ColorGradient? colorGradient = value as ColorGradient; + GradientStops collection = new(); + if (colorGradient == null) return collection; - } - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - GradientStopCollection? collection = value as GradientStopCollection; - ColorGradient colorGradients = new(); - if (collection == null) - return colorGradients; + foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position)) + collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position)); + return collection; + } - foreach (GradientStop c in collection.OrderBy(s => s.Offset)) - colorGradients.Add(new ColorGradientStop(new SKColor(c.Color.R, c.Color.G, c.Color.B, c.Color.A), (float) c.Offset)); + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + GradientStops? collection = value as GradientStops; + ColorGradient colorGradients = new(); + if (collection == null) return colorGradients; - } + + foreach (GradientStop c in collection.OrderBy(s => s.Offset)) + colorGradients.Add(new ColorGradientStop(new SKColor(c.Color.R, c.Color.G, c.Color.B, c.Color.A), (float) c.Offset)); + return colorGradients; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs b/src/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs index 6f612d002..b09052e65 100644 --- a/src/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs +++ b/src/Artemis.UI.Shared/Converters/ColorToSKColorConverter.cs @@ -1,30 +1,38 @@ using System; using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; +using Avalonia.Data.Converters; +using Avalonia.Media; +using FluentAvalonia.UI.Media; using SkiaSharp; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Converters { - /// /// - /// Converts into a . + /// Converts into . /// - [ValueConversion(typeof(Color), typeof(SKColor))] - public class SKColorToColorConverter : IValueConverter + public class ColorToSKColorConverter : IValueConverter { /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - SKColor skColor = (SKColor) value; - return Color.FromArgb(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue); + if (value is Color avaloniaColor) + return new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A); + if (value is Color2 fluentAvaloniaColor) + return new SKColor(fluentAvaloniaColor.R, fluentAvaloniaColor.G, fluentAvaloniaColor.B, fluentAvaloniaColor.A); + + return SKColor.Empty; } /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { - Color color = (Color) value; - return new SKColor(color.R, color.G, color.B, color.A); + Color result = new(0, 0, 0, 0); + if (value is SKColor skColor) + result = new Color(skColor.Alpha, skColor.Red, skColor.Green, skColor.Blue); + + if (targetType == typeof(Color2)) + return (Color2) result; + return result; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/ColorToSolidColorConverter.cs b/src/Artemis.UI.Shared/Converters/ColorToSolidColorConverter.cs deleted file mode 100644 index 85fc3bdd0..000000000 --- a/src/Artemis.UI.Shared/Converters/ColorToSolidColorConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; - -namespace Artemis.UI.Shared -{ - /// - /// - /// Converts into a with full - /// opacity. - /// - [ValueConversion(typeof(Color), typeof(string))] - public class ColorToSolidColorConverter : IValueConverter - { - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - Color color = (Color) value; - return Color.FromRgb(color.R, color.G, color.B); - } - - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/ColorToStringConverter.cs b/src/Artemis.UI.Shared/Converters/ColorToStringConverter.cs deleted file mode 100644 index 4519d8b83..000000000 --- a/src/Artemis.UI.Shared/Converters/ColorToStringConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; - -namespace Artemis.UI.Shared -{ - /// - /// - /// Converts into . - /// - [ValueConversion(typeof(Color), typeof(string))] - public class ColorToStringConverter : IValueConverter - { - /// - public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) - { - return value?.ToString()?.ToUpper(); - } - - /// - public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) - { - try - { - if (string.IsNullOrWhiteSpace(value as string)) - return default(Color); - - object? color = ColorConverter.ConvertFromString((string) value!); - if (color is Color c) - return c; - - return default(Color); - } - catch (FormatException) - { - return default(Color); - } - } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs b/src/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs rename to src/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs diff --git a/src/Artemis.UI.Shared/Converters/IntToVisibilityConverter.cs b/src/Artemis.UI.Shared/Converters/IntToVisibilityConverter.cs deleted file mode 100644 index 2db2cd50a..000000000 --- a/src/Artemis.UI.Shared/Converters/IntToVisibilityConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace Artemis.UI.Shared -{ - /// - /// Converts to - /// - public class IntToVisibilityConverter : IValueConverter - { - /// - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - Parameters direction; - if (parameter == null) - direction = Parameters.Normal; - else - direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter); - - return direction == Parameters.Normal - ? value is > 1 ? Visibility.Visible : Visibility.Collapsed - : value is > 1 ? Visibility.Collapsed : Visibility.Visible; - } - - /// - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - - private enum Parameters - { - Normal, - Inverted - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/NullToVisibilityConverter.cs b/src/Artemis.UI.Shared/Converters/NullToVisibilityConverter.cs deleted file mode 100644 index dd70fbac2..000000000 --- a/src/Artemis.UI.Shared/Converters/NullToVisibilityConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Globalization; -using System.Windows; -using System.Windows.Data; - -namespace Artemis.UI.Shared -{ - /// - /// Converts to - /// - public class NullToVisibilityConverter : IValueConverter - { - /// - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - Parameters direction; - if (parameter == null) - direction = Parameters.Normal; - else - direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter); - - if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) - value = null; - - if (direction == Parameters.Normal) - return value == null ? Visibility.Collapsed : Visibility.Visible; - return value == null ? Visibility.Visible : Visibility.Collapsed; - } - - /// - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - - private enum Parameters - { - Normal, - Inverted - } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs b/src/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs rename to src/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToBrushConverter.cs b/src/Artemis.UI.Shared/Converters/SKColorToBrushConverter.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Converters/SKColorToBrushConverter.cs rename to src/Artemis.UI.Shared/Converters/SKColorToBrushConverter.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs b/src/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs rename to src/Artemis.UI.Shared/Converters/SKColorToColorConverter.cs diff --git a/src/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs b/src/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs index d1dbfcde8..7837c188e 100644 --- a/src/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs +++ b/src/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs @@ -1,31 +1,28 @@ using System; using System.Globalization; -using System.Windows.Data; -using System.Windows.Media; +using Avalonia.Data.Converters; using SkiaSharp; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Converters; + +/// +/// +/// Converts into . +/// +public class SKColorToStringConverter : IValueConverter { /// - /// - /// Converts into . - /// - [ValueConversion(typeof(Color), typeof(string))] - public class SKColorToStringConverter : IValueConverter + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - /// - public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) - { - return value?.ToString(); - } + return value?.ToString(); + } - /// - public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) - { - if (string.IsNullOrWhiteSpace(value as string)) - return SKColor.Empty; + /// + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (string.IsNullOrWhiteSpace(value as string)) + return SKColor.Empty; - return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty; - } + return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs b/src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs deleted file mode 100644 index 9e4c7cb20..000000000 --- a/src/Artemis.UI.Shared/Converters/StreamToBitmapImageConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Windows.Data; -using System.Windows.Media.Imaging; - -namespace Artemis.UI.Shared -{ - /// - /// - /// Converts bitmap file in the form of a into . - /// - [ValueConversion(typeof(Stream), typeof(BitmapImage))] - public class StreamToBitmapImageConverter : IValueConverter - { - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is not Stream stream) - return Binding.DoNothing; - - stream.Position = 0; - - BitmapImage selectedBitmap = new(); - selectedBitmap.BeginInit(); - selectedBitmap.StreamSource = stream; - selectedBitmap.CacheOption = BitmapCacheOption.OnLoad; - selectedBitmap.EndInit(); - selectedBitmap.Freeze(); - - stream.Position = 0; - return selectedBitmap; - } - - - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return Binding.DoNothing; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/StreamToSvgImageConverter.cs b/src/Artemis.UI.Shared/Converters/StreamToSvgImageConverter.cs deleted file mode 100644 index e157daa1f..000000000 --- a/src/Artemis.UI.Shared/Converters/StreamToSvgImageConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Windows.Data; -using System.Windows.Media.Imaging; -using SharpVectors.Converters; -using SharpVectors.Renderers.Wpf; - -namespace Artemis.UI.Shared -{ - /// - /// - /// Converts SVG file in the form of a into . - /// - [ValueConversion(typeof(Stream), typeof(BitmapImage))] - public class StreamToSvgImageConverter : IValueConverter - { - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - if (value is not Stream stream) - return Binding.DoNothing; - - stream.Position = 0; - - StreamSvgConverter converter = new(new WpfDrawingSettings()); - using MemoryStream imageStream = new(); - converter.Convert(stream, imageStream); - - BitmapImage selectedBitmap = new(); - selectedBitmap.BeginInit(); - selectedBitmap.StreamSource = imageStream; - selectedBitmap.CacheOption = BitmapCacheOption.OnLoad; - selectedBitmap.EndInit(); - selectedBitmap.Freeze(); - - stream.Position = 0; - return selectedBitmap; - } - - - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return Binding.DoNothing; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/StringToNumericConverter.cs b/src/Artemis.UI.Shared/Converters/StringToNumericConverter.cs deleted file mode 100644 index ced2709ae..000000000 --- a/src/Artemis.UI.Shared/Converters/StringToNumericConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Globalization; -using System.Windows.Data; -using Artemis.Core; - -namespace Artemis.UI.Shared -{ - /// - /// Converts into . - /// - [ValueConversion(typeof(string), typeof(Numeric))] - public class StringToNumericConverter : IValueConverter - { - /// - public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) - { - return value?.ToString(); - } - - /// - public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) - { - Numeric.TryParse(value as string, out Numeric result); - return result; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/TypeToStringConverter.cs b/src/Artemis.UI.Shared/Converters/TypeToStringConverter.cs index 10570fa8a..8bec6ce70 100644 --- a/src/Artemis.UI.Shared/Converters/TypeToStringConverter.cs +++ b/src/Artemis.UI.Shared/Converters/TypeToStringConverter.cs @@ -1,9 +1,9 @@ using System; using System.Globalization; -using System.Windows.Data; using Artemis.Core; +using Avalonia.Data.Converters; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Converters { /// /// Converts into . @@ -11,7 +11,7 @@ namespace Artemis.UI.Shared public class TypeToStringConverter : IValueConverter { /// - public object? Convert(object value, Type targetType, object? parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { bool humanizeProvided = bool.TryParse(parameter?.ToString(), out bool humanize); if (value is Type type) @@ -21,7 +21,7 @@ namespace Artemis.UI.Shared } /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs b/src/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs rename to src/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs index abbc4171d..6dd1d3ded 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/DataModelDisplayViewModel.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; using Artemis.Core.Modules; -using Stylet; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization { /// /// Represents a display view model @@ -10,8 +10,7 @@ namespace Artemis.UI.Shared /// The type of the data model public abstract class DataModelDisplayViewModel : DataModelDisplayViewModel { - [AllowNull] - private T _displayValue = default!; + [AllowNull] private T _displayValue = default!; /// /// Gets or sets value that the view model must display @@ -22,7 +21,8 @@ namespace Artemis.UI.Shared get => _displayValue; set { - if (!SetAndNotify(ref _displayValue, value)) return; + if (Equals(value, _displayValue)) return; + RaiseAndSetIfChanged(ref _displayValue, value); OnDisplayValueUpdated(); } } @@ -46,7 +46,7 @@ namespace Artemis.UI.Shared /// /// For internal use only, implement instead. /// - public abstract class DataModelDisplayViewModel : PropertyChangedBase + public abstract class DataModelDisplayViewModel : ViewModelBase { private DataModelPropertyAttribute? _propertyDescription; @@ -56,7 +56,7 @@ namespace Artemis.UI.Shared public DataModelPropertyAttribute? PropertyDescription { get => _propertyDescription; - internal set => SetAndNotify(ref _propertyDescription, value); + internal set => RaiseAndSetIfChanged(ref _propertyDescription, value); } /// diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs index 1c9574dfb..cd3bd505c 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/DataModelInputViewModel.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Data; -using System.Windows.Input; using Artemis.Core.Modules; -using Stylet; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization { /// /// Represents a input view model @@ -37,7 +33,7 @@ namespace Artemis.UI.Shared public T InputValue { get => _inputValue; - set => SetAndNotify(ref _inputValue, value); + set => this.RaiseAndSetIfChanged(ref _inputValue, value); } /// @@ -54,9 +50,6 @@ namespace Artemis.UI.Shared return; _closed = true; - foreach (BindingExpressionBase sourceUpdatingBinding in BindingOperations.GetSourceUpdatingBindings(View)) - sourceUpdatingBinding.UpdateSource(); - OnSubmit(); UpdateCallback(InputValue, true); } @@ -76,7 +69,7 @@ namespace Artemis.UI.Shared /// /// For internal use only, implement instead. /// - public abstract class DataModelInputViewModel : PropertyChangedBase, IViewAware + public abstract class DataModelInputViewModel : ReactiveObject { /// /// Prevents this type being implemented directly, implement instead. @@ -117,24 +110,5 @@ namespace Artemis.UI.Shared protected virtual void OnCancel() { } - - /// - public void AttachView(UIElement view) - { - if (View != null) - throw new InvalidOperationException(string.Format("Tried to attach View {0} to ViewModel {1}, but it already has a view attached", view.GetType().Name, GetType().Name)); - - View = view; - - // After the animation finishes attempt to focus the input field - Task.Run(async () => - { - await Task.Delay(50); - await Execute.OnUIThreadAsync(() => View.MoveFocus(new TraversalRequest(FocusNavigationDirection.First))); - }); - } - - /// - public UIElement? View { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs b/src/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs index 310063ef0..e867a2995 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/DataModelVisualizationRegistration.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using Artemis.Core; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization { /// /// Represents a layer brush registered through diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs index 55ccdf424..c6b3b1478 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelEventViewModel.cs @@ -3,8 +3,10 @@ using System.Linq; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared { /// /// Represents a view model that visualizes an event data model property @@ -23,7 +25,7 @@ namespace Artemis.UI.Shared public Type? DisplayValueType { get => _displayValueType; - set => SetAndNotify(ref _displayValueType, value); + set => this.RaiseAndSetIfChanged(ref _displayValueType, value); } /// diff --git a/src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs rename to src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListItemViewModel.cs diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs deleted file mode 100644 index d9060f943..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertiesViewModel.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Linq; -using Artemis.Core; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Shared -{ - /// - /// Represents a view model that wraps a regular but contained in - /// a - /// - public class DataModelListPropertiesViewModel : DataModelPropertiesViewModel - { - private object? _displayValue; - private int _index; - private Type? _listType; - - internal DataModelListPropertiesViewModel(Type listType, string? name) : base(null, null, null) - { - ListType = listType; - } - - /// - /// Gets the index of the element within the list - /// - public int Index - { - get => _index; - set => SetAndNotify(ref _index, value); - } - - /// - /// Gets the type of elements contained in the list - /// - public Type? ListType - { - get => _listType; - set => SetAndNotify(ref _listType, value); - } - - /// - /// Gets the value of the property that is being visualized - /// - public new object? DisplayValue - { - get => _displayValue; - set => SetAndNotify(ref _displayValue, value); - } - - /// - /// Gets the view model that handles displaying the property - /// - public DataModelVisualizationViewModel? DisplayViewModel => Children.FirstOrDefault(); - - /// - public override string? DisplayPath => null; - - /// - public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration) - { - PopulateProperties(dataModelUIService, configuration); - if (DisplayViewModel == null) - return; - - if (IsVisualizationExpanded && !DisplayViewModel.IsVisualizationExpanded) - DisplayViewModel.IsVisualizationExpanded = IsVisualizationExpanded; - DisplayViewModel.Update(dataModelUIService, null); - } - - /// - public override object? GetCurrentValue() - { - return DisplayValue; - } - - /// - public override string ToString() - { - return $"[List item {Index}] {DisplayPath ?? Path}"; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs deleted file mode 100644 index 29bdeb259..000000000 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListPropertyViewModel.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using Artemis.Core; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Shared -{ - /// - /// Represents a view model that visualizes a single data model property contained in a - /// - /// - public class DataModelListPropertyViewModel : DataModelPropertyViewModel - { - private int _index; - private Type? _listType; - - internal DataModelListPropertyViewModel(Type listType, DataModelDisplayViewModel displayViewModel, string? name) : base(null, null, null) - { - ListType = listType; - DisplayViewModel = displayViewModel; - } - - internal DataModelListPropertyViewModel(Type listType, string? name) : base(null, null, null) - { - ListType = listType; - } - - /// - /// Gets the index of the element within the list - /// - public int Index - { - get => _index; - internal set => SetAndNotify(ref _index, value); - } - - /// - /// Gets the type of elements contained in the list - /// - public Type? ListType - { - get => _listType; - private set => SetAndNotify(ref _listType, value); - } - - /// - public override object? GetCurrentValue() - { - return DisplayValue; - } - - /// - public override void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration) - { - // Display value gets updated by parent, don't do anything if it is null - if (DisplayValue == null) - return; - - if (DisplayViewModel == null) - DisplayViewModel = dataModelUIService.GetDataModelDisplayViewModel(DisplayValue.GetType(), PropertyDescription, true); - - ListType = DisplayValue.GetType(); - DisplayViewModel?.UpdateValue(DisplayValue); - } - - /// - public override string ToString() - { - return $"[List item {Index}] {DisplayPath ?? Path} - {DisplayValue}"; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs index 3605c43a0..9ec4838d9 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelListViewModel.cs @@ -1,12 +1,13 @@ using System; using System.Collections; -using System.Windows.Documents; +using System.Collections.ObjectModel; using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; -using Stylet; +using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared { /// /// Represents a view model that visualizes a list data model property @@ -16,14 +17,14 @@ namespace Artemis.UI.Shared private string _countDisplay; private Type? _displayValueType; private IEnumerable? _list; - private BindableCollection _listChildren; + private ObservableCollection _listChildren; private int _listCount; internal DataModelListViewModel(DataModel dataModel, DataModelVisualizationViewModel parent, DataModelPath dataModelPath) : base(dataModel, parent, dataModelPath) { _countDisplay = "0 items"; - _listChildren = new BindableCollection(); + _listChildren = new ObservableCollection(); } /// @@ -32,7 +33,7 @@ namespace Artemis.UI.Shared public IEnumerable? List { get => _list; - private set => SetAndNotify(ref _list, value); + private set => this.RaiseAndSetIfChanged(ref _list, value); } /// @@ -41,7 +42,7 @@ namespace Artemis.UI.Shared public int ListCount { get => _listCount; - private set => SetAndNotify(ref _listCount, value); + private set => this.RaiseAndSetIfChanged(ref _listCount, value); } /// @@ -50,7 +51,7 @@ namespace Artemis.UI.Shared public Type? DisplayValueType { get => _displayValueType; - set => SetAndNotify(ref _displayValueType, value); + set => this.RaiseAndSetIfChanged(ref _displayValueType, value); } /// @@ -59,16 +60,16 @@ namespace Artemis.UI.Shared public string CountDisplay { get => _countDisplay; - set => SetAndNotify(ref _countDisplay, value); + set => this.RaiseAndSetIfChanged(ref _countDisplay, value); } /// /// Gets a list of child view models that visualize the elements in the list /// - public BindableCollection ListChildren + public ObservableCollection ListChildren { get => _listChildren; - private set => SetAndNotify(ref _listChildren, value); + private set => this.RaiseAndSetIfChanged(ref _listChildren, value); } /// @@ -97,22 +98,15 @@ namespace Artemis.UI.Shared ListChildren.Add(child); } else - { child = ListChildren[index]; - } - if (child is DataModelListPropertiesViewModel dataModelListClassViewModel) - { - dataModelListClassViewModel.DisplayValue = item; - dataModelListClassViewModel.Index = index; - } - else if (child is DataModelListPropertyViewModel dataModelListPropertyViewModel) + if (child is DataModelListItemViewModel dataModelListPropertyViewModel) { dataModelListPropertyViewModel.DisplayValue = item; dataModelListPropertyViewModel.Index = index; + dataModelListPropertyViewModel.Update(dataModelUIService, configuration); } - child.Update(dataModelUIService, configuration); index++; } @@ -134,16 +128,10 @@ namespace Artemis.UI.Shared { // If a display VM was found, prefer to use that in any case DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(listType, PropertyDescription); - if (typeViewModel != null) - return new DataModelListPropertyViewModel(listType, typeViewModel, name); - // For primitives, create a property view model, it may be null that is fine - if (listType.IsPrimitive || listType.IsEnum || listType == typeof(string)) - return new DataModelListPropertyViewModel(listType, name); - // For other value types create a child view model - if (listType.IsClass || listType.IsStruct()) - return new DataModelListPropertiesViewModel(listType, name); - return null; + return typeViewModel != null + ? new DataModelListItemViewModel(listType, typeViewModel, name) + : new DataModelListItemViewModel(listType, name); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs index 423af79e9..e162f0599 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertiesViewModel.cs @@ -2,8 +2,10 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared { /// /// Represents a view model that visualizes a class (POCO) data model property containing child properties @@ -24,7 +26,7 @@ namespace Artemis.UI.Shared public Type? DisplayValueType { get => _displayValueType; - private set => SetAndNotify(ref _displayValueType, value); + private set => this.RaiseAndSetIfChanged(ref _displayValueType, value); } /// @@ -33,7 +35,7 @@ namespace Artemis.UI.Shared public object? DisplayValue { get => _displayValue; - private set => SetAndNotify(ref _displayValue, value); + private set => this.RaiseAndSetIfChanged(ref _displayValue, value); } /// diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs index 5f4b35891..f6215d7af 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelPropertyViewModel.cs @@ -2,8 +2,10 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared { /// /// Represents a view model that visualizes a single data model property contained in a @@ -26,7 +28,7 @@ namespace Artemis.UI.Shared public object? DisplayValue { get => _displayValue; - internal set => SetAndNotify(ref _displayValue, value); + internal set => this.RaiseAndSetIfChanged(ref _displayValue, value); } /// @@ -35,7 +37,7 @@ namespace Artemis.UI.Shared public Type? DisplayValueType { get => _displayValueType; - protected set => SetAndNotify(ref _displayValueType, value); + protected set => this.RaiseAndSetIfChanged(ref _displayValueType, value); } /// @@ -44,7 +46,7 @@ namespace Artemis.UI.Shared public DataModelDisplayViewModel? DisplayViewModel { get => _displayViewModel; - internal set => SetAndNotify(ref _displayViewModel, value); + internal set => this.RaiseAndSetIfChanged(ref _displayViewModel, value); } /// diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs index bd4747b1d..fe41f0644 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelUpdateConfiguration.cs @@ -1,4 +1,4 @@ -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared { /// /// Represents a configuration to use while updating a diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index 17432d696..9a212bae1 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -6,368 +6,396 @@ using System.Reflection; using System.Text; using Artemis.Core; using Artemis.Core.Modules; -using Artemis.UI.Shared.Services; -using Stylet; +using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.DataModelVisualization.Shared; + +/// +/// Represents a base class for a view model that visualizes a part of the data model +/// +public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposable { - /// - /// Represents a base class for a view model that visualizes a part of the data model - /// - public abstract class DataModelVisualizationViewModel : PropertyChangedBase, IDisposable + private const int MaxDepth = 4; + private ObservableCollection _children; + private DataModel? _dataModel; + private bool _isMatchingFilteredTypes; + private bool _isVisualizationExpanded; + private DataModelVisualizationViewModel? _parent; + private bool _populatedStaticChildren; + private DataModelPropertyAttribute? _propertyDescription; + + internal DataModelVisualizationViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath) { - private const int MaxDepth = 4; - private BindableCollection _children; - private DataModel? _dataModel; - private bool _isMatchingFilteredTypes; - private bool _isVisualizationExpanded; - private DataModelVisualizationViewModel? _parent; - private DataModelPropertyAttribute? _propertyDescription; - private bool _populatedStaticChildren; + _dataModel = dataModel; + _children = new ObservableCollection(); + _parent = parent; + DataModelPath = dataModelPath; + IsMatchingFilteredTypes = true; - internal DataModelVisualizationViewModel(DataModel? dataModel, DataModelVisualizationViewModel? parent, DataModelPath? dataModelPath) + if (parent == null) + IsRootViewModel = true; + else + PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription; + } + + /// + /// Gets a boolean indicating whether this view model is at the root of the data model + /// + public bool IsRootViewModel { get; protected set; } + + /// + /// Gets the data model path to the property this view model is visualizing + /// + public DataModelPath? DataModelPath { get; } + + /// + /// Gets a string representation of the path backing this model + /// + public string? Path => DataModelPath?.Path; + + /// + /// Gets the property depth of the view model + /// + public int Depth { get; private set; } + + /// + /// Gets the data model backing this view model + /// + public DataModel? DataModel + { + get => _dataModel; + protected set => this.RaiseAndSetIfChanged(ref _dataModel, value); + } + + /// + /// Gets the property description of the property this view model is visualizing + /// + public DataModelPropertyAttribute? PropertyDescription + { + get => _propertyDescription; + protected set => this.RaiseAndSetIfChanged(ref _propertyDescription, value); + } + + /// + /// Gets the parent of this view model + /// + public DataModelVisualizationViewModel? Parent + { + get => _parent; + protected set => this.RaiseAndSetIfChanged(ref _parent, value); + } + + /// + /// Gets or sets an observable collection containing the children of this view model + /// + public ObservableCollection Children + { + get => _children; + set => this.RaiseAndSetIfChanged(ref _children, value); + } + + /// + /// Gets a boolean indicating whether the property being visualized matches the types last provided to + /// + /// + public bool IsMatchingFilteredTypes + { + get => _isMatchingFilteredTypes; + private set => this.RaiseAndSetIfChanged(ref _isMatchingFilteredTypes, value); + } + + /// + /// Gets or sets a boolean indicating whether the visualization is expanded, exposing the + /// + public bool IsVisualizationExpanded + { + get => _isVisualizationExpanded; + set { - _dataModel = dataModel; - _children = new BindableCollection(); - _parent = parent; - DataModelPath = dataModelPath; - IsMatchingFilteredTypes = true; - - if (parent == null) - IsRootViewModel = true; - else - PropertyDescription = DataModelPath?.GetPropertyDescription() ?? DataModel?.DataModelDescription; + if (!this.RaiseAndSetIfChanged(ref _isVisualizationExpanded, value)) return; + RequestUpdate(); } + } - /// - /// Gets a boolean indicating whether this view model is at the root of the data model - /// - public bool IsRootViewModel { get; protected set; } + /// + /// Gets a user-friendly representation of the + /// + public virtual string? DisplayPath => DataModelPath != null + ? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier)) + : null; - /// - /// Gets the data model path to the property this view model is visualizing - /// - public DataModelPath? DataModelPath { get; } - - /// - /// Gets a string representation of the path backing this model - /// - public string? Path => DataModelPath?.Path; - - /// - /// Gets the property depth of the view model - /// - public int Depth { get; private set; } - - /// - /// Gets the data model backing this view model - /// - public DataModel? DataModel - { - get => _dataModel; - protected set => SetAndNotify(ref _dataModel, value); - } - - /// - /// Gets the property description of the property this view model is visualizing - /// - public DataModelPropertyAttribute? PropertyDescription - { - get => _propertyDescription; - protected set => SetAndNotify(ref _propertyDescription, value); - } - - /// - /// Gets the parent of this view model - /// - public DataModelVisualizationViewModel? Parent - { - get => _parent; - protected set => SetAndNotify(ref _parent, value); - } - - /// - /// Gets or sets a bindable collection containing the children of this view model - /// - public BindableCollection Children - { - get => _children; - set => SetAndNotify(ref _children, value); - } - - /// - /// Gets a boolean indicating whether the property being visualized matches the types last provided to - /// - /// - public bool IsMatchingFilteredTypes - { - get => _isMatchingFilteredTypes; - private set => SetAndNotify(ref _isMatchingFilteredTypes, value); - } - - /// - /// Gets or sets a boolean indicating whether the visualization is expanded, exposing the - /// - public bool IsVisualizationExpanded - { - get => _isVisualizationExpanded; - set - { - if (!SetAndNotify(ref _isVisualizationExpanded, value)) return; - RequestUpdate(); - } - } - - /// - /// Gets a user-friendly representation of the - /// - public virtual string? DisplayPath => DataModelPath != null - ? string.Join(" › ", DataModelPath.Segments.Select(s => s.GetPropertyDescription()?.Name ?? s.Identifier)) - : null; - - /// - /// Updates the datamodel and if in an parent, any children - /// - /// The data model UI service used during update - /// The configuration to apply while updating - public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration); - - /// - /// Gets the current value of the property being visualized - /// - /// The current value of the property being visualized - public virtual object? GetCurrentValue() - { - if (IsRootViewModel) - return null; - - return DataModelPath?.GetValue(); - } - - /// - /// Determines whether the provided types match the type of the property being visualized and sets the result in - /// - /// - /// Whether the type may be a loose match, meaning it can be cast or converted - /// The types to filter - public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes) - { - if (filteredTypes != null) - { - if (filteredTypes.All(t => t == null)) - filteredTypes = null; - else - filteredTypes = filteredTypes.Where(t => t != null).ToArray(); - } - - // If the VM has children, its own type is not relevant - if (Children.Any()) - { - foreach (DataModelVisualizationViewModel child in Children) - child?.ApplyTypeFilter(looseMatch, filteredTypes); - - IsMatchingFilteredTypes = true; - return; - } - - // If null is passed, clear the type filter - if (filteredTypes == null || filteredTypes.Length == 0) - { - IsMatchingFilteredTypes = true; - return; - } - - // If the type couldn't be retrieved either way, assume false - Type? type = DataModelPath?.GetPropertyType(); - if (type == null) - { - IsMatchingFilteredTypes = false; - return; - } - - if (looseMatch) - IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) || - t == typeof(Enum) && type.IsEnum || - t == typeof(IEnumerable<>) && type.IsGenericEnumerable() || - type.IsGenericType && t == type.GetGenericTypeDefinition()); - else - IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum); - } - - internal virtual int GetChildDepth() - { - return 0; - } - - internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? dataModelUpdateConfiguration) - { - if (IsRootViewModel && DataModel == null) - return; - - Type? modelType = IsRootViewModel ? DataModel?.GetType() : DataModelPath?.GetPropertyType(); - if (modelType == null) - throw new ArtemisSharedUIException("Failed to populate data model visualization properties, couldn't get a property type"); - - // Add missing static children only once, they're static after all - if (!_populatedStaticChildren) - { - foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) - { - string childPath = AppendToPath(propertyInfo.Name); - if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) - continue; - if (propertyInfo.GetCustomAttribute() != null) - continue; - MethodInfo? getMethod = propertyInfo.GetGetMethod(); - if (getMethod == null || getMethod.GetParameters().Any()) - continue; - - DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); - if (child != null) - Children.Add(child); - } - - _populatedStaticChildren = true; - } - - // Remove static children that should be hidden - if (DataModel != null) - { - ReadOnlyCollection hiddenProperties = DataModel.GetHiddenProperties(); - foreach (PropertyInfo hiddenProperty in hiddenProperties) - { - string childPath = AppendToPath(hiddenProperty.Name); - DataModelVisualizationViewModel? toRemove = Children.FirstOrDefault(c => c.Path != null && c.Path == childPath); - if (toRemove != null) - Children.Remove(toRemove); - } - } - - // Add missing dynamic children - object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue(); - if (value is DataModel dataModel) - { - foreach (string key in dataModel.DynamicChildren.Keys.ToList()) - { - string childPath = AppendToPath(key); - if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) - continue; - - DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); - if (child != null) - Children.Add(child); - } - } - - // Remove dynamic children that have been removed from the data model - List toRemoveDynamic = Children.Where(c => c.DataModelPath != null && !c.DataModelPath.IsValid).ToList(); - if (toRemoveDynamic.Any()) - Children.RemoveRange(toRemoveDynamic); - } - - private DataModelVisualizationViewModel? CreateChild(IDataModelUIService dataModelUIService, string path, int depth) - { - if (DataModel == null) - throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model"); - if (depth > MaxDepth) - return null; - - DataModelPath dataModelPath = new(DataModel, path); - if (!dataModelPath.IsValid) - return null; - - PropertyInfo? propertyInfo = dataModelPath.GetPropertyInfo(); - Type? propertyType = dataModelPath.GetPropertyType(); - - // Skip properties decorated with DataModelIgnore - if (propertyInfo != null && Attribute.IsDefined(propertyInfo, typeof(DataModelIgnoreAttribute))) - return null; - // Skip properties that are in the ignored properties list of the respective profile module/data model expansion - if (DataModel.GetHiddenProperties().Any(p => p.Equals(propertyInfo))) - return null; - - if (propertyType == null) - return null; - - // If a display VM was found, prefer to use that in any case - DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription); - if (typeViewModel != null) - return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth}; - // For primitives, create a property view model, it may be null that is fine - if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) - return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth}; - if (propertyType.IsGenericEnumerable()) - return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth}; - if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>)) - return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth}; - // For other value types create a child view model - if (propertyType.IsClass || propertyType.IsStruct()) - return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth}; + /// + /// Updates the datamodel and if in an parent, any children + /// + /// The data model UI service used during update + /// The configuration to apply while updating + public abstract void Update(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? configuration); + /// + /// Gets the current value of the property being visualized + /// + /// The current value of the property being visualized + public virtual object? GetCurrentValue() + { + if (IsRootViewModel) return null; + + return DataModelPath?.GetValue(); + } + + /// + /// Determines whether the provided types match the type of the property being visualized and sets the result in + /// + /// + /// Whether the type may be a loose match, meaning it can be cast or converted + /// The types to filter + public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes) + { + if (filteredTypes != null) + { + if (filteredTypes.All(t => t == null)) + filteredTypes = null; + else + filteredTypes = filteredTypes.Where(t => t != null).ToArray(); } - private string AppendToPath(string toAppend) + // If the VM has children, its own type is not relevant + if (Children.Any()) { - if (string.IsNullOrEmpty(Path)) - return toAppend; + foreach (DataModelVisualizationViewModel child in Children) + child?.ApplyTypeFilter(looseMatch, filteredTypes); - StringBuilder builder = new(); - builder.Append(Path); - builder.Append("."); - builder.Append(toAppend); - return builder.ToString(); + IsMatchingFilteredTypes = true; + return; } - private void RequestUpdate() + // If null is passed, clear the type filter + if (filteredTypes == null || filteredTypes.Length == 0) { - Parent?.RequestUpdate(); - OnUpdateRequested(); + IsMatchingFilteredTypes = true; + return; } - #region Events - - /// - /// Occurs when an update to the property this view model visualizes is requested - /// - public event EventHandler? UpdateRequested; - - /// - /// Invokes the event - /// - protected virtual void OnUpdateRequested() + // If the type couldn't be retrieved either way, assume false + Type? type = DataModelPath?.GetPropertyType(); + if (type == null) { - UpdateRequested?.Invoke(this, EventArgs.Empty); + IsMatchingFilteredTypes = false; + return; } - #endregion + if (looseMatch) + IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) || + t == typeof(Enum) && type.IsEnum || + t == typeof(IEnumerable<>) && type.IsGenericEnumerable() || + type.IsGenericType && t == type.GetGenericTypeDefinition()); + else + IsMatchingFilteredTypes = filteredTypes.Any(t => t == type || t == typeof(Enum) && type.IsEnum); + } - #region IDisposable + /// + /// Occurs when an update to the property this view model visualizes is requested + /// + public event EventHandler? UpdateRequested; - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) + /// + /// Expands this view model and any children to expose the provided . + /// + /// The data model path to expose. + public void ExpandToPath(DataModelPath dataModelPath) + { + if (dataModelPath.Target != DataModel) + throw new ArtemisSharedUIException("Can't expand to a path that doesn't belong to this data model."); + + IsVisualizationExpanded = true; + DataModelPathSegment current = dataModelPath.Segments.Skip(1).First(); + Children.FirstOrDefault(c => c.Path == current.Path)?.ExpandToPath(current.Next); + } + + /// + /// Finds the view model that hosts the given path. + /// + /// The path to find + /// The matching view model, may be null if the path doesn't exist or isn't expanded + public DataModelVisualizationViewModel? GetViewModelForPath(DataModelPath dataModelPath) + { + if (dataModelPath.Target != DataModel) + throw new ArtemisSharedUIException("Can't expand to a path that doesn't belong to this data model."); + + if (DataModelPath?.Path == dataModelPath.Path) + return this; + + return Children.Select(c => c.GetViewModelForPath(dataModelPath)).FirstOrDefault(match => match != null); + } + + /// + /// Invokes the event + /// + protected virtual void OnUpdateRequested() + { + UpdateRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) + DataModelPath?.Dispose(); + foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children) + dataModelVisualizationViewModel.Dispose(true); + } + } + + internal virtual int GetChildDepth() + { + return 0; + } + + internal void PopulateProperties(IDataModelUIService dataModelUIService, DataModelUpdateConfiguration? dataModelUpdateConfiguration) + { + if (IsRootViewModel && DataModel == null) + return; + + Type? modelType = IsRootViewModel ? DataModel?.GetType() : DataModelPath?.GetPropertyType(); + if (modelType == null) + throw new ArtemisSharedUIException("Failed to populate data model visualization properties, couldn't get a property type"); + + // Add missing static children only once, they're static after all + if (!_populatedStaticChildren) + { + foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) { - DataModelPath?.Dispose(); - foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in Children) - dataModelVisualizationViewModel.Dispose(true); + string childPath = AppendToPath(propertyInfo.Name); + if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath))) + continue; + if (propertyInfo.GetCustomAttribute() != null) + continue; + MethodInfo? getMethod = propertyInfo.GetGetMethod(); + if (getMethod == null || getMethod.GetParameters().Any()) + continue; + + DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); + if (child != null) + Children.Add(child); + } + + _populatedStaticChildren = true; + } + + // Remove static children that should be hidden + if (DataModel != null) + { + ReadOnlyCollection hiddenProperties = DataModel.GetHiddenProperties(); + foreach (PropertyInfo hiddenProperty in hiddenProperties) + { + string childPath = AppendToPath(hiddenProperty.Name); + DataModelVisualizationViewModel? toRemove = Children.FirstOrDefault(c => c.Path != null && c.Path == childPath); + if (toRemove != null) + Children.Remove(toRemove); } } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + // Add missing dynamic children + object? value = Parent == null || Parent.IsRootViewModel ? DataModel : DataModelPath?.GetValue(); + if (value is DataModel dataModel) + foreach (string key in dataModel.DynamicChildren.Keys.ToList()) + { + string childPath = AppendToPath(key); + if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) + continue; - #endregion + DataModelVisualizationViewModel? child = CreateChild(dataModelUIService, childPath, GetChildDepth()); + if (child != null) + Children.Add(child); + } + + // Remove dynamic children that have been removed from the data model + List toRemoveDynamic = Children.Where(c => c.DataModelPath != null && !c.DataModelPath.IsValid).ToList(); + foreach (DataModelVisualizationViewModel dataModelVisualizationViewModel in toRemoveDynamic) + Children.Remove(dataModelVisualizationViewModel); + } + + private DataModelVisualizationViewModel? CreateChild(IDataModelUIService dataModelUIService, string path, int depth) + { + if (DataModel == null) + throw new ArtemisSharedUIException("Cannot create a data model visualization child VM for a parent without a data model"); + if (depth > MaxDepth) + return null; + + DataModelPath dataModelPath = new(DataModel, path); + if (!dataModelPath.IsValid) + return null; + + PropertyInfo? propertyInfo = dataModelPath.GetPropertyInfo(); + Type? propertyType = dataModelPath.GetPropertyType(); + + // Skip properties decorated with DataModelIgnore + if (propertyInfo != null && Attribute.IsDefined(propertyInfo, typeof(DataModelIgnoreAttribute))) + return null; + // Skip properties that are in the ignored properties list of the respective profile module/data model expansion + if (DataModel.GetHiddenProperties().Any(p => p.Equals(propertyInfo))) + return null; + + if (propertyType == null) + return null; + + // If a display VM was found, prefer to use that in any case + DataModelDisplayViewModel? typeViewModel = dataModelUIService.GetDataModelDisplayViewModel(propertyType, PropertyDescription); + if (typeViewModel != null) + return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {DisplayViewModel = typeViewModel, Depth = depth}; + // For primitives, create a property view model, it may be null that is fine + if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) + return new DataModelPropertyViewModel(DataModel, this, dataModelPath) {Depth = depth}; + if (propertyType.IsGenericEnumerable()) + return new DataModelListViewModel(DataModel, this, dataModelPath) {Depth = depth}; + if (propertyType == typeof(DataModelEvent) || propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(DataModelEvent<>)) + return new DataModelEventViewModel(DataModel, this, dataModelPath) {Depth = depth}; + // For other value types create a child view model + if (propertyType.IsClass || propertyType.IsStruct()) + return new DataModelPropertiesViewModel(DataModel, this, dataModelPath) {Depth = depth}; + + return null; + } + + private string AppendToPath(string toAppend) + { + if (string.IsNullOrEmpty(Path)) + return toAppend; + + StringBuilder builder = new(); + builder.Append(Path); + builder.Append("."); + builder.Append(toAppend); + return builder.ToString(); + } + + private void RequestUpdate() + { + Parent?.RequestUpdate(); + OnUpdateRequested(); + } + + private void ExpandToPath(DataModelPathSegment? segment) + { + if (segment == null) + return; + + IsVisualizationExpanded = true; + Children.FirstOrDefault(c => c.Path == segment.Path)?.ExpandToPath(segment.Next); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml b/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml rename to src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml diff --git a/src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml.cs b/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml.cs rename to src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayView.axaml.cs diff --git a/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs b/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs index cced51797..40a265190 100644 --- a/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs +++ b/src/Artemis.UI.Shared/DefaultTypes/DataModel/Display/DefaultDataModelDisplayViewModel.cs @@ -1,4 +1,8 @@ using System; +using Artemis.Core; +using Artemis.UI.Shared.DataModelVisualization; +using Newtonsoft.Json; +using ReactiveUI; namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display { @@ -8,33 +12,33 @@ namespace Artemis.UI.Shared.DefaultTypes.DataModel.Display /// internal class DefaultDataModelDisplayViewModel : DataModelDisplayViewModel { - private bool _showNull; - private bool _showToString; + private readonly JsonSerializerSettings _serializerSettings; + private string _display; - internal DefaultDataModelDisplayViewModel() + public DefaultDataModelDisplayViewModel() { - ShowNull = true; + _serializerSettings = new JsonSerializerSettings() + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + PreserveReferencesHandling = PreserveReferencesHandling.None + }; + _display = "null"; } - public bool ShowToString + public string Display { - get => _showToString; - private set => SetAndNotify(ref _showToString, value); + get => _display; + set => RaiseAndSetIfChanged(ref _display, value); } - - public bool ShowNull - { - get => _showNull; - set => SetAndNotify(ref _showNull, value); - } - + protected override void OnDisplayValueUpdated() { if (DisplayValue is Enum enumDisplayValue) - DisplayValue = EnumUtilities.HumanizeValue(enumDisplayValue); - - ShowToString = DisplayValue != null; - ShowNull = DisplayValue == null; + Display = EnumUtilities.HumanizeValue(enumDisplayValue); + else if (DisplayValue is not string) + Display = JsonConvert.SerializeObject(DisplayValue, _serializerSettings); + else + Display = DisplayValue?.ToString() ?? "null"; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/DependencyProperties/SizeObserver.cs b/src/Artemis.UI.Shared/DependencyProperties/SizeObserver.cs deleted file mode 100644 index 9f085f67b..000000000 --- a/src/Artemis.UI.Shared/DependencyProperties/SizeObserver.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Windows; - -namespace Artemis.UI.Shared -{ - /// - /// Provides a dependency property that can observe the size of an element and apply it to bindings - /// - public static class SizeObserver - { - /// - /// Gets or sets whether the element should be observed - /// - public static readonly DependencyProperty ObserveProperty = DependencyProperty.RegisterAttached( - "Observe", - typeof(bool), - typeof(SizeObserver), - new FrameworkPropertyMetadata(OnObserveChanged)); - - /// - /// Gets or sets the observed width of the element - /// - public static readonly DependencyProperty ObservedWidthProperty = DependencyProperty.RegisterAttached( - "ObservedWidth", - typeof(double), - typeof(SizeObserver)); - - /// - /// Gets or sets the observed height of the element - /// - public static readonly DependencyProperty ObservedHeightProperty = DependencyProperty.RegisterAttached( - "ObservedHeight", - typeof(double), - typeof(SizeObserver)); - - /// - /// Gets whether the provided is being observed - /// - public static bool GetObserve(FrameworkElement frameworkElement) - { - return (bool) frameworkElement.GetValue(ObserveProperty); - } - - /// - /// Sets whether the provided is being observed - /// - public static void SetObserve(FrameworkElement frameworkElement, bool observe) - { - frameworkElement.SetValue(ObserveProperty, observe); - } - - /// - /// Gets the observed width of the the provided - /// - public static double GetObservedWidth(FrameworkElement frameworkElement) - { - return (double) frameworkElement.GetValue(ObservedWidthProperty); - } - - /// - /// Sets the observed width of the the provided - /// - public static void SetObservedWidth(FrameworkElement frameworkElement, double observedWidth) - { - frameworkElement.SetValue(ObservedWidthProperty, observedWidth); - } - - /// - /// Gets the observed height of the the provided - /// - public static double GetObservedHeight(FrameworkElement frameworkElement) - { - return (double) frameworkElement.GetValue(ObservedHeightProperty); - } - - /// - /// Sets the observed height of the the provided - /// - public static void SetObservedHeight(FrameworkElement frameworkElement, double observedHeight) - { - frameworkElement.SetValue(ObservedHeightProperty, observedHeight); - } - - private static void OnObserveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) - { - FrameworkElement frameworkElement = (FrameworkElement) dependencyObject; - - if ((bool) e.NewValue) - { - frameworkElement.SizeChanged += OnFrameworkElementSizeChanged; - UpdateObservedSizesForFrameworkElement(frameworkElement); - } - else - { - frameworkElement.SizeChanged -= OnFrameworkElementSizeChanged; - } - } - - private static void OnFrameworkElementSizeChanged(object sender, SizeChangedEventArgs e) - { - UpdateObservedSizesForFrameworkElement((FrameworkElement) sender); - } - - private static void UpdateObservedSizesForFrameworkElement(FrameworkElement frameworkElement) - { - frameworkElement.SetCurrentValue(ObservedWidthProperty, frameworkElement.ActualWidth); - frameworkElement.SetCurrentValue(ObservedHeightProperty, frameworkElement.ActualHeight); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs b/src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs index 9306237ed..7f59aedad 100644 --- a/src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs +++ b/src/Artemis.UI.Shared/Events/DataModelSelectedEventArgs.cs @@ -2,7 +2,7 @@ using Artemis.Core; using Artemis.UI.Shared.Controls; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Events { /// /// Provides data about selection events raised by diff --git a/src/Avalonia/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs b/src/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs rename to src/Artemis.UI.Shared/Events/DialogClosedEventArgs.cs diff --git a/src/Artemis.UI.Shared/Events/LedClickedEventArgs.cs b/src/Artemis.UI.Shared/Events/LedClickedEventArgs.cs index 3455b9800..cfaa9a160 100644 --- a/src/Artemis.UI.Shared/Events/LedClickedEventArgs.cs +++ b/src/Artemis.UI.Shared/Events/LedClickedEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Events { /// /// Provides data on LED click events raised by the device visualizer diff --git a/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs b/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs index 3ec9ac748..fc79d770b 100644 --- a/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs +++ b/src/Artemis.UI.Shared/Events/ProfileConfigurationEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Events { /// /// Provides data on profile related events raised by the profile editor diff --git a/src/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs b/src/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs index e2555f3ab..67cd4d5ba 100644 --- a/src/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs +++ b/src/Artemis.UI.Shared/Events/RenderProfileElementEventArgs.cs @@ -1,7 +1,7 @@ using System; using Artemis.Core; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Events { /// /// Provides data on profile element related events raised by the profile editor diff --git a/src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs b/src/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs rename to src/Artemis.UI.Shared/Events/SelectionRectangleEventArgs.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/ControlExtensions.cs b/src/Artemis.UI.Shared/Extensions/ControlExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/ControlExtensions.cs rename to src/Artemis.UI.Shared/Extensions/ControlExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs b/src/Artemis.UI.Shared/Extensions/LayerExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/LayerExtensions.cs rename to src/Artemis.UI.Shared/Extensions/LayerExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs b/src/Artemis.UI.Shared/Extensions/PointExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/PointExtensions.cs rename to src/Artemis.UI.Shared/Extensions/PointExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs b/src/Artemis.UI.Shared/Extensions/SKColorExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/SKColorExtensions.cs rename to src/Artemis.UI.Shared/Extensions/SKColorExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/SKMatrixExtensions.cs b/src/Artemis.UI.Shared/Extensions/SKMatrixExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/SKMatrixExtensions.cs rename to src/Artemis.UI.Shared/Extensions/SKMatrixExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/SKRectExtensions.cs b/src/Artemis.UI.Shared/Extensions/SKRectExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/SKRectExtensions.cs rename to src/Artemis.UI.Shared/Extensions/SKRectExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/TypeExtensions.cs b/src/Artemis.UI.Shared/Extensions/TypeExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/TypeExtensions.cs rename to src/Artemis.UI.Shared/Extensions/TypeExtensions.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Extensions/VisualExtensions.cs b/src/Artemis.UI.Shared/Extensions/VisualExtensions.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Extensions/VisualExtensions.cs rename to src/Artemis.UI.Shared/Extensions/VisualExtensions.cs diff --git a/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs b/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs deleted file mode 100644 index 228b34c6e..000000000 --- a/src/Artemis.UI.Shared/Ninject/Factories/ISharedVMFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Artemis.UI.Shared -{ - /// - /// Represents a factory for view models provided by the Artemis Shared UI library - /// - public interface ISharedVmFactory - { - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs b/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs index 328e6fe06..b3469aa43 100644 --- a/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs +++ b/src/Artemis.UI.Shared/Ninject/SharedUIModule.cs @@ -1,10 +1,9 @@ using System; -using Artemis.UI.Shared.Services; -using MaterialDesignThemes.Wpf; +using Artemis.UI.Shared.Services.Interfaces; using Ninject.Extensions.Conventions; using Ninject.Modules; -namespace Artemis.UI.Shared +namespace Artemis.UI.Shared.Ninject { /// /// The main of the Artemis Shared UI toolkit that binds all services @@ -27,17 +26,6 @@ namespace Artemis.UI.Shared .BindAllInterfaces() .Configure(c => c.InSingletonScope()); }); - - Kernel.Bind().ToConstant(new SnackbarMessageQueue(TimeSpan.FromSeconds(5))).InSingletonScope(); - - // Bind UI factories - Kernel.Bind(x => - { - x.FromThisAssembly() - .SelectAllInterfaces() - .InheritedFrom() - .BindToFactory(); - }); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs b/src/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs index 0c61885b0..73f045f94 100644 --- a/src/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs +++ b/src/Artemis.UI.Shared/Plugins/LayerBrushes/BrushConfigurationViewModel.cs @@ -1,12 +1,13 @@ -using Artemis.Core.LayerBrushes; -using Stylet; +using System; +using System.Threading.Tasks; +using Artemis.Core.LayerBrushes; namespace Artemis.UI.Shared.LayerBrushes { /// /// Represents a view model for a brush configuration window /// - public abstract class BrushConfigurationViewModel : Screen + public abstract class BrushConfigurationViewModel : ActivatableViewModelBase { /// /// Creates a new instance of the class @@ -17,19 +18,40 @@ namespace Artemis.UI.Shared.LayerBrushes LayerBrush = layerBrush; } - /// - /// Creates a new instance of the class with a validator - /// - /// - /// - protected BrushConfigurationViewModel(BaseLayerBrush layerBrush, IModelValidator validator) : base(validator) - { - LayerBrush = layerBrush; - } - /// /// Gets the layer brush this view model is associated with /// public BaseLayerBrush LayerBrush { get; } + + /// + /// Closes the dialog + /// + public void RequestClose() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// if the window may close; otherwise . + public virtual bool CanClose() + { + return true; + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// A task if the window may close; otherwise . + public virtual Task CanCloseAsync() + { + return Task.FromResult(true); + } + + /// + /// Occurs when a close was requested + /// + public event EventHandler? CloseRequested; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs b/src/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs index daf80ed91..7f7b94fa8 100644 --- a/src/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs +++ b/src/Artemis.UI.Shared/Plugins/LayerBrushes/LayerBrushConfigurationDialog.cs @@ -38,7 +38,7 @@ namespace Artemis.UI.Shared.LayerBrushes public int DialogHeight { get; set; } = 800; /// - /// The type of view model the tab contains + /// The type of view model the dialog contains /// public abstract Type Type { get; } } diff --git a/src/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs b/src/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs index aac795039..2645faddc 100644 --- a/src/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs +++ b/src/Artemis.UI.Shared/Plugins/LayerEffects/EffectConfigurationViewModel.cs @@ -1,35 +1,57 @@ -using Artemis.Core.LayerEffects; -using Stylet; +using System; +using System.Threading.Tasks; +using Artemis.Core.LayerEffects; +using Avalonia.Threading; -namespace Artemis.UI.Shared.LayerEffects +namespace Artemis.UI.Shared.LayerEffects; + +/// +/// Represents a view model for an effect configuration window +/// +public abstract class EffectConfigurationViewModel : ActivatableViewModelBase { /// - /// Represents a view model for an effect configuration window + /// Creates a new instance of the class /// - public abstract class EffectConfigurationViewModel : Screen + /// + protected EffectConfigurationViewModel(BaseLayerEffect layerEffect) { - /// - /// Creates a new instance of the class - /// - /// - protected EffectConfigurationViewModel(BaseLayerEffect layerEffect) - { - LayerEffect = layerEffect; - } - - /// - /// Creates a new instance of the class with a validator - /// - /// - /// - protected EffectConfigurationViewModel(BaseLayerEffect layerEffect, IModelValidator validator) : base(validator) - { - LayerEffect = layerEffect; - } - - /// - /// Gets the layer effect this view model is associated with - /// - public BaseLayerEffect LayerEffect { get; } + LayerEffect = layerEffect; } + + /// + /// Gets the layer effect this view model is associated with + /// + public BaseLayerEffect LayerEffect { get; } + + /// + /// Closes the dialog + /// + public void RequestClose() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// if the window may close; otherwise . + public virtual bool CanClose() + { + return true; + } + + /// + /// Called when the window wants to close, returning will cause the window to stay open. + /// + /// A task if the window may close; otherwise . + public virtual Task CanCloseAsync() + { + return Task.FromResult(true); + } + + /// + /// Occurs when a close was requested + /// + public event EventHandler? CloseRequested; } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs b/src/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs index bb043fa93..f5810e74c 100644 --- a/src/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs +++ b/src/Artemis.UI.Shared/Plugins/LayerEffects/LayerEffectConfigurationDialog.cs @@ -39,7 +39,7 @@ namespace Artemis.UI.Shared.LayerEffects public int DialogHeight { get; set; } = 800; /// - /// The type of view model the tab contains + /// The type of view model the dialog contains /// public abstract Type Type { get; } } diff --git a/src/Artemis.UI.Shared/Plugins/Modules/ModuleViewModel.cs b/src/Artemis.UI.Shared/Plugins/Modules/ModuleViewModel.cs deleted file mode 100644 index 05222aed1..000000000 --- a/src/Artemis.UI.Shared/Plugins/Modules/ModuleViewModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Artemis.Core.Modules; -using Stylet; - -namespace Artemis.UI.Shared.Modules -{ - /// - /// The base class for any view model that belongs to a module - /// - public abstract class ModuleViewModel : Screen, IModuleViewModel - { - /// - /// The base class for any view model that belongs to a module - /// - /// The module this view model belongs to - /// The name of the tab that's shown on the modules UI page - protected ModuleViewModel(Module module, string displayName) - { - Module = module; - DisplayName = displayName.ToUpper(); - } - - /// - /// Gets the module this view model belongs to - /// - public Module Module { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs b/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs index 23f1fd0ed..cbc4f7413 100644 --- a/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs +++ b/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs @@ -15,9 +15,7 @@ namespace Artemis.UI.Shared /// public abstract class PluginConfigurationDialog : IPluginConfigurationDialog { - /// - /// The type of view model the tab contains - /// + /// public abstract Type Type { get; } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs b/src/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs index 61e88a503..4fcfaa4fd 100644 --- a/src/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs +++ b/src/Artemis.UI.Shared/Plugins/PluginConfigurationViewModel.cs @@ -1,12 +1,12 @@ -using Artemis.Core; -using Stylet; +using System; +using Artemis.Core; namespace Artemis.UI.Shared { /// /// Represents a view model for a plugin configuration window /// - public abstract class PluginConfigurationViewModel : Screen, IPluginConfigurationViewModel + public abstract class PluginConfigurationViewModel : ViewModelValidationBase, IPluginConfigurationViewModel { /// /// Creates a new instance of the class @@ -17,19 +17,30 @@ namespace Artemis.UI.Shared Plugin = plugin; } - /// - /// Creates a new instance of the class with a validator - /// - /// - /// - protected PluginConfigurationViewModel(Plugin plugin, IModelValidator validator) : base(validator) - { - Plugin = plugin; - } - /// /// Gets the plugin this configuration view model is associated with /// public Plugin Plugin { get; } + + /// + /// Closes the window hosting the view model + /// + public void Close() + { + CloseRequested?.Invoke(this, EventArgs.Empty); + OnCloseRequested(); + } + + /// + /// Called when the the window hosting the view model should close + /// + public virtual void OnCloseRequested() + { + } + + /// + /// Occurs when the the window hosting the view model should close + /// + public event EventHandler? CloseRequested; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs b/src/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs index 957c8429e..416d2614a 100644 --- a/src/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs +++ b/src/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs @@ -1,12 +1,12 @@ using Artemis.Core.ScriptingProviders; -using Stylet; +using ReactiveUI; namespace Artemis.UI.Shared.ScriptingProviders { /// /// Represents a Stylet view model containing a script editor /// - public class ScriptEditorViewModel : Screen, IScriptEditorViewModel + public class ScriptEditorViewModel : ActivatableViewModelBase, IScriptEditorViewModel { private Script? _script; @@ -42,7 +42,7 @@ namespace Artemis.UI.Shared.ScriptingProviders public Script? Script { get => _script; - internal set => SetAndNotify(ref _script, value); + internal set => RaiseAndSetIfChanged(ref _script, value); } /// diff --git a/src/Artemis.UI.Shared/Properties/Annotations.cs b/src/Artemis.UI.Shared/Properties/Annotations.cs deleted file mode 100644 index 227caf7b8..000000000 --- a/src/Artemis.UI.Shared/Properties/Annotations.cs +++ /dev/null @@ -1,1383 +0,0 @@ -/* MIT License - -Copyright (c) 2016 JetBrains http://www.jetbrains.com - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. */ - -using System; - -#pragma warning disable 8618 -#pragma warning disable 1591 - -// ReSharper disable InheritdocConsiderUsage -// ReSharper disable UnusedMember.Global -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedAutoPropertyAccessor.Global -// ReSharper disable IntroduceOptionalParameters.Global -// ReSharper disable MemberCanBeProtected.Global -// ReSharper disable InconsistentNaming - -namespace Artemis.UI.Shared.Properties -{ - /// - /// Indicates that the value of the marked element could be null sometimes, - /// so checking for null is required before its usage. - /// - /// - /// - /// [CanBeNull] object Test() => null; - /// - /// void UseTest() { - /// var p = Test(); - /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] - public sealed class CanBeNullAttribute : Attribute - { - } - - /// - /// Indicates that the value of the marked element can never be null. - /// - /// - /// - /// [NotNull] object Foo() { - /// return null; // Warning: Possible 'null' assignment - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] - public sealed class NotNullAttribute : Attribute - { - } - - /// - /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task - /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property - /// or of the Lazy.Value property can never be null. - /// - /// - /// - /// public void Foo([ItemNotNull]List<string> books) - /// { - /// foreach (var book in books) { - /// if (book != null) // Warning: Expression is always true - /// Console.WriteLine(book.ToUpper()); - /// } - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field)] - public sealed class ItemNotNullAttribute : Attribute - { - } - - /// - /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task - /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property - /// or of the Lazy.Value property can be null. - /// - /// - /// - /// public void Foo([ItemCanBeNull]List<string> books) - /// { - /// foreach (var book in books) - /// { - /// // Warning: Possible 'System.NullReferenceException' - /// Console.WriteLine(book.ToUpper()); - /// } - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | - AttributeTargets.Delegate | AttributeTargets.Field)] - public sealed class ItemCanBeNullAttribute : Attribute - { - } - - /// - /// Indicates that the marked method builds string by the format pattern and (optional) arguments. - /// The parameter, which contains the format string, should be given in constructor. The format string - /// should be in -like form. - /// - /// - /// - /// [StringFormatMethod("message")] - /// void ShowError(string message, params object[] args) { /* do something */ } - /// - /// void Foo() { - /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Constructor | AttributeTargets.Method | - AttributeTargets.Property | AttributeTargets.Delegate)] - public sealed class StringFormatMethodAttribute : Attribute - { - /// - /// Specifies which parameter of an annotated method should be treated as the format string - /// - public StringFormatMethodAttribute([NotNull] string formatParameterName) - { - FormatParameterName = formatParameterName; - } - - [NotNull] - public string FormatParameterName { get; } - } - - /// - /// Use this annotation to specify a type that contains static or const fields - /// with values for the annotated property/field/parameter. - /// The specified type will be used to improve completion suggestions. - /// - /// - /// - /// namespace TestNamespace - /// { - /// public class Constants - /// { - /// public static int INT_CONST = 1; - /// public const string STRING_CONST = "1"; - /// } - /// - /// public class Class1 - /// { - /// [ValueProvider("TestNamespace.Constants")] public int myField; - /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } - /// - /// public void Test() - /// { - /// Foo(/*try completion here*/);// - /// myField = /*try completion here*/ - /// } - /// } - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, - AllowMultiple = true)] - public sealed class ValueProviderAttribute : Attribute - { - public ValueProviderAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; } - } - - /// - /// Indicates that the function argument should be a string literal and match one - /// of the parameters of the caller function. For example, ReSharper annotates - /// the parameter of . - /// - /// - /// - /// void Foo(string param) { - /// if (param == null) - /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol - /// } - /// - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class InvokerParameterNameAttribute : Attribute - { - } - - /// - /// Indicates that the method is contained in a type that implements - /// System.ComponentModel.INotifyPropertyChanged interface and this method - /// is used to notify that some property value changed. - /// - /// - /// The method should be non-static and conform to one of the supported signatures: - /// - /// - /// NotifyChanged(string) - /// - /// - /// NotifyChanged(params string[]) - /// - /// - /// NotifyChanged{T}(Expression{Func{T}}) - /// - /// - /// NotifyChanged{T,U}(Expression{Func{T,U}}) - /// - /// - /// SetProperty{T}(ref T, T, string) - /// - /// - /// - /// - /// - /// public class Foo : INotifyPropertyChanged { - /// public event PropertyChangedEventHandler PropertyChanged; - /// - /// [NotifyPropertyChangedInvocator] - /// protected virtual void NotifyChanged(string propertyName) { ... } - /// - /// string _name; - /// - /// public string Name { - /// get { return _name; } - /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } - /// } - /// } - /// - /// Examples of generated notifications: - /// - /// - /// NotifyChanged("Property") - /// - /// - /// NotifyChanged(() => Property) - /// - /// - /// NotifyChanged((VM x) => x.Property) - /// - /// - /// SetProperty(ref myField, value, "Property") - /// - /// - /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class NotifyPropertyChangedInvocatorAttribute : Attribute - { - public NotifyPropertyChangedInvocatorAttribute() - { - } - - public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) - { - ParameterName = parameterName; - } - - [CanBeNull] - public string ParameterName { get; } - } - - /// - /// Describes dependency between method input and output. - /// - /// - ///

Function Definition Table syntax:

- /// - /// FDT ::= FDTRow [;FDTRow]* - /// FDTRow ::= Input => Output | Output <= Input - /// Input ::= ParameterName: Value [, Input]* - /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} - /// Value ::= true | false | null | notnull | canbenull - /// - /// If the method has a single input parameter, its name could be omitted.
- /// Using halt (or void/nothing, which is the same) for the method output - /// means that the method doesn't return normally (throws or terminates the process).
- /// Value canbenull is only applicable for output parameters.
- /// You can use multiple [ContractAnnotation] for each FDT row, or use single attribute - /// with rows separated by semicolon. There is no notion of order rows, all rows are checked - /// for applicability and applied per each program state tracked by the analysis engine.
- ///
- /// - /// - /// - /// - /// [ContractAnnotation("=> halt")] - /// public void TerminationMethod() - /// - /// - /// - /// - /// [ContractAnnotation("null <= param:null")] // reverse condition syntax - /// public string GetName(string surname) - /// - /// - /// - /// - /// [ContractAnnotation("s:null => true")] - /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() - /// - /// - /// - /// - /// // A method that returns null if the parameter is null, - /// // and not null if the parameter is not null - /// [ContractAnnotation("null => null; notnull => notnull")] - /// public object Transform(object data) - /// - /// - /// - /// - /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] - /// public bool TryParse(string s, out Person result) - /// - /// - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public sealed class ContractAnnotationAttribute : Attribute - { - public ContractAnnotationAttribute([NotNull] string contract) - : this(contract, false) - { - } - - public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) - { - Contract = contract; - ForceFullStates = forceFullStates; - } - - [NotNull] - public string Contract { get; } - - public bool ForceFullStates { get; } - } - - /// - /// Indicates whether the marked element should be localized. - /// - /// - /// - /// [LocalizationRequiredAttribute(true)] - /// class Foo { - /// string str = "my string"; // Warning: Localizable string - /// } - /// - /// - [AttributeUsage(AttributeTargets.All)] - public sealed class LocalizationRequiredAttribute : Attribute - { - public LocalizationRequiredAttribute() : this(true) - { - } - - public LocalizationRequiredAttribute(bool required) - { - Required = required; - } - - public bool Required { get; } - } - - /// - /// Indicates that the value of the marked type (or its derivatives) - /// cannot be compared using '==' or '!=' operators and Equals() - /// should be used instead. However, using '==' or '!=' for comparison - /// with null is always permitted. - /// - /// - /// - /// [CannotApplyEqualityOperator] - /// class NoEquality { } - /// - /// class UsesNoEquality { - /// void Test() { - /// var ca1 = new NoEquality(); - /// var ca2 = new NoEquality(); - /// if (ca1 != null) { // OK - /// bool condition = ca1 == ca2; // Warning - /// } - /// } - /// } - /// - /// - [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] - public sealed class CannotApplyEqualityOperatorAttribute : Attribute - { - } - - /// - /// When applied to a target attribute, specifies a requirement for any type marked - /// with the target attribute to implement or inherit specific type or types. - /// - /// - /// - /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement - /// class ComponentAttribute : Attribute { } - /// - /// [Component] // ComponentAttribute requires implementing IComponent interface - /// class MyComponent : IComponent { } - /// - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - [BaseTypeRequired(typeof(Attribute))] - public sealed class BaseTypeRequiredAttribute : Attribute - { - public BaseTypeRequiredAttribute([NotNull] Type baseType) - { - BaseType = baseType; - } - - [NotNull] - public Type BaseType { get; } - } - - /// - /// Indicates that the marked symbol is used implicitly (e.g. via reflection, in external library), - /// so this symbol will not be reported as unused (as well as by other usage inspections). - /// - [AttributeUsage(AttributeTargets.All, Inherited = false)] - public sealed class UsedImplicitlyAttribute : Attribute - { - public UsedImplicitlyAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) - { - } - - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) - { - } - - public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) - { - } - - public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - public ImplicitUseKindFlags UseKindFlags { get; } - - public ImplicitUseTargetFlags TargetFlags { get; } - } - - /// - /// Can be applied to attributes, type parameters, and parameters of a type assignable from - /// . - /// When applied to an attribute, the decorated attribute behaves the same as . - /// When applied to a type parameter or to a parameter of type , indicates that the - /// corresponding type - /// is used implicitly. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] - public sealed class MeansImplicitUseAttribute : Attribute - { - public MeansImplicitUseAttribute() - : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) - { - } - - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) - : this(useKindFlags, ImplicitUseTargetFlags.Default) - { - } - - public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) - : this(ImplicitUseKindFlags.Default, targetFlags) - { - } - - public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) - { - UseKindFlags = useKindFlags; - TargetFlags = targetFlags; - } - - [UsedImplicitly] - public ImplicitUseKindFlags UseKindFlags { get; } - - [UsedImplicitly] - public ImplicitUseTargetFlags TargetFlags { get; } - } - - /// - /// Specify the details of implicitly used symbol when it is marked - /// with or . - /// - [Flags] - public enum ImplicitUseKindFlags - { - Default = Access | Assign | InstantiatedWithFixedConstructorSignature, - - /// Only entity marked with attribute considered used. - Access = 1, - - /// Indicates implicit assignment to a member. - Assign = 2, - - /// - /// Indicates implicit instantiation of a type with fixed constructor signature. - /// That means any unused constructor parameters won't be reported as such. - /// - InstantiatedWithFixedConstructorSignature = 4, - - /// Indicates implicit instantiation of a type. - InstantiatedNoFixedConstructorSignature = 8 - } - - /// - /// Specify what is considered to be used implicitly when marked - /// with or . - /// - [Flags] - public enum ImplicitUseTargetFlags - { - Default = Itself, - Itself = 1, - - /// Members of entity marked with attribute are considered used. - Members = 2, - - /// Entity marked with attribute and all its members considered used. - WithMembers = Itself | Members - } - - /// - /// This attribute is intended to mark publicly available API - /// which should not be removed and so is treated as used. - /// - [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] - [AttributeUsage(AttributeTargets.All, Inherited = false)] - public sealed class PublicAPIAttribute : Attribute - { - public PublicAPIAttribute() - { - } - - public PublicAPIAttribute([NotNull] string comment) - { - Comment = comment; - } - - [CanBeNull] - public string Comment { get; } - } - - /// - /// Tells code analysis engine if the parameter is completely handled when the invoked method is on stack. - /// If the parameter is a delegate, indicates that delegate is executed while the method is executed. - /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class InstantHandleAttribute : Attribute - { - } - - /// - /// Indicates that a method does not make any observable state changes. - /// The same as System.Diagnostics.Contracts.PureAttribute. - /// - /// - /// - /// [Pure] int Multiply(int x, int y) => x * y; - /// - /// void M() { - /// Multiply(123, 42); // Waring: Return value of pure method is not used - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class PureAttribute : Attribute - { - } - - /// - /// Indicates that the return value of the method invocation must be used. - /// - /// - /// Methods decorated with this attribute (in contrast to pure methods) might change state, - /// but make no sense without using their return value.
- /// Similarly to , this attribute - /// will help detecting usages of the method when the return value in not used. - /// Additionally, you can optionally specify a custom message, which will be used when showing warnings, e.g. - /// [MustUseReturnValue("Use the return value to...")]. - ///
- [AttributeUsage(AttributeTargets.Method)] - public sealed class MustUseReturnValueAttribute : Attribute - { - public MustUseReturnValueAttribute() - { - } - - public MustUseReturnValueAttribute([NotNull] string justification) - { - Justification = justification; - } - - [CanBeNull] - public string Justification { get; } - } - - /// - /// Indicates the type member or parameter of some type, that should be used instead of all other ways - /// to get the value of that type. This annotation is useful when you have some "context" value evaluated - /// and stored somewhere, meaning that all other ways to get this value must be consolidated with existing one. - /// - /// - /// - /// class Foo { - /// [ProvidesContext] IBarService _barService = ...; - /// - /// void ProcessNode(INode node) { - /// DoSomething(node, node.GetGlobalServices().Bar); - /// // ^ Warning: use value of '_barService' field - /// } - /// } - /// - /// - [AttributeUsage( - AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] - public sealed class ProvidesContextAttribute : Attribute - { - } - - /// - /// Indicates that a parameter is a path to a file or a folder within a web project. - /// Path can be relative or absolute, starting from web root (~). - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class PathReferenceAttribute : Attribute - { - public PathReferenceAttribute() - { - } - - public PathReferenceAttribute([NotNull] [PathReference] string basePath) - { - BasePath = basePath; - } - - [CanBeNull] - public string BasePath { get; } - } - - /// - /// An extension method marked with this attribute is processed by code completion - /// as a 'Source Template'. When the extension method is completed over some expression, its source code - /// is automatically expanded like a template at call site. - /// - /// - /// Template method body can contain valid source code and/or special comments starting with '$'. - /// Text inside these comments is added as source code when the template is applied. Template parameters - /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. - /// Use the attribute to specify macros for parameters. - /// - /// - /// In this example, the 'forEach' method is a source template available over all values - /// of enumerable types, producing ordinary C# 'foreach' statement and placing caret inside block: - /// - /// [SourceTemplate] - /// public static void forEach<T>(this IEnumerable<T> xs) { - /// foreach (var x in xs) { - /// //$ $END$ - /// } - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class SourceTemplateAttribute : Attribute - { - } - - /// - /// Allows specifying a macro for a parameter of a source template. - /// - /// - /// You can apply the attribute on the whole method or on any of its additional parameters. The macro expression - /// is defined in the property. When applied on a method, the target - /// template parameter is defined in the property. To apply the macro silently - /// for the parameter, set the property value = -1. - /// - /// - /// Applying the attribute on a source template method: - /// - /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] - /// public static void forEach<T>(this IEnumerable<T> collection) { - /// foreach (var item in collection) { - /// //$ $END$ - /// } - /// } - /// - /// Applying the attribute on a template method parameter: - /// - /// [SourceTemplate] - /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { - /// /*$ var $x$Id = "$newguid$" + x.ToString(); - /// x.DoSomething($x$Id); */ - /// } - /// - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] - public sealed class MacroAttribute : Attribute - { - /// - /// Allows specifying a macro that will be executed for a source template - /// parameter when the template is expanded. - /// - [CanBeNull] - public string Expression { get; set; } - - /// - /// Allows specifying which occurrence of the target parameter becomes editable when the template is deployed. - /// - /// - /// If the target parameter is used several times in the template, only one occurrence becomes editable; - /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, - /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. - /// - public int Editable { get; set; } - - /// - /// Identifies the target parameter of a source template if the - /// is applied on a template method. - /// - [CanBeNull] - public string Target { get; set; } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] - public sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute - { - public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] - public sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute - { - public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] - public sealed class AspMvcAreaViewLocationFormatAttribute : Attribute - { - public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] - public sealed class AspMvcMasterLocationFormatAttribute : Attribute - { - public AspMvcMasterLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] - public sealed class AspMvcPartialViewLocationFormatAttribute : Attribute - { - public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; } - } - - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] - public sealed class AspMvcViewLocationFormatAttribute : Attribute - { - public AspMvcViewLocationFormatAttribute([NotNull] string format) - { - Format = format; - } - - [NotNull] - public string Format { get; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC action. If applied to a method, the MVC action name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcActionAttribute : Attribute - { - public AspMvcActionAttribute() - { - } - - public AspMvcActionAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [CanBeNull] - public string AnonymousProperty { get; } - } - - /// - /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcAreaAttribute : Attribute - { - public AspMvcAreaAttribute() - { - } - - public AspMvcAreaAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [CanBeNull] - public string AnonymousProperty { get; } - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is - /// an MVC controller. If applied to a method, the MVC controller name is calculated - /// implicitly from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcControllerAttribute : Attribute - { - public AspMvcControllerAttribute() - { - } - - public AspMvcControllerAttribute([NotNull] string anonymousProperty) - { - AnonymousProperty = anonymousProperty; - } - - [CanBeNull] - public string AnonymousProperty { get; } - } - - /// - /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute - /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcMasterAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute - /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class AspMvcModelTypeAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC - /// partial view. If applied to a method, the MVC partial view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcPartialViewAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public sealed class AspMvcSuppressViewErrorAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcDisplayTemplateAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. - /// Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcEditorTemplateAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. - /// Use this attribute for custom wrappers similar to - /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcTemplateAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly - /// from the context. Use this attribute for custom wrappers similar to - /// System.Web.Mvc.Controller.View(Object). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcViewAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view component name. - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcViewComponentAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter - /// is an MVC view component view. If applied to a method, the MVC view component view name is default. - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class AspMvcViewComponentViewAttribute : Attribute - { - } - - /// - /// ASP.NET MVC attribute. When applied to a parameter of an attribute, - /// indicates that this parameter is an MVC action name. - /// - /// - /// - /// [ActionName("Foo")] - /// public ActionResult Login(string returnUrl) { - /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK - /// return RedirectToAction("Bar"); // Error: Cannot resolve action - /// } - /// - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] - public sealed class AspMvcActionSelectorAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] - public sealed class HtmlElementAttributesAttribute : Attribute - { - public HtmlElementAttributesAttribute() - { - } - - public HtmlElementAttributesAttribute([NotNull] string name) - { - Name = name; - } - - [CanBeNull] - public string Name { get; } - } - - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] - public sealed class HtmlAttributeValueAttribute : Attribute - { - public HtmlAttributeValueAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; } - } - - /// - /// Razor attribute. Indicates that the marked parameter or method is a Razor section. - /// Use this attribute for custom wrappers similar to - /// System.Web.WebPages.WebPageBase.RenderSection(String). - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] - public sealed class RazorSectionAttribute : Attribute - { - } - - /// - /// Indicates how method, constructor invocation, or property access - /// over collection type affects the contents of the collection. - /// Use to specify the access type. - /// - /// - /// Using this attribute only makes sense if all collection methods are marked with this attribute. - /// - /// - /// - /// public class MyStringCollection : List<string> - /// { - /// [CollectionAccess(CollectionAccessType.Read)] - /// public string GetFirstString() - /// { - /// return this.ElementAt(0); - /// } - /// } - /// class Test - /// { - /// public void Foo() - /// { - /// // Warning: Contents of the collection is never updated - /// var col = new MyStringCollection(); - /// string x = col.GetFirstString(); - /// } - /// } - /// - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] - public sealed class CollectionAccessAttribute : Attribute - { - public CollectionAccessAttribute(CollectionAccessType collectionAccessType) - { - CollectionAccessType = collectionAccessType; - } - - public CollectionAccessType CollectionAccessType { get; } - } - - /// - /// Provides a value for the to define - /// how the collection method invocation affects the contents of the collection. - /// - [Flags] - public enum CollectionAccessType - { - /// Method does not use or modify content of the collection. - None = 0, - - /// Method only reads content of the collection but does not modify it. - Read = 1, - - /// Method can change content of the collection but does not add new elements. - ModifyExistingContent = 2, - - /// Method can add new elements to the collection. - UpdatedContent = ModifyExistingContent | 4 - } - - /// - /// Indicates that the marked method is assertion method, i.e. it halts the control flow if - /// one of the conditions is satisfied. To set the condition, mark one of the parameters with - /// attribute. - /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class AssertionMethodAttribute : Attribute - { - } - - /// - /// Indicates the condition parameter of the assertion method. The method itself should be - /// marked by attribute. The mandatory argument of - /// the attribute is the assertion type. - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class AssertionConditionAttribute : Attribute - { - public AssertionConditionAttribute(AssertionConditionType conditionType) - { - ConditionType = conditionType; - } - - public AssertionConditionType ConditionType { get; } - } - - /// - /// Specifies assertion type. If the assertion method argument satisfies the condition, - /// then the execution continues. Otherwise, execution is assumed to be halted. - /// - public enum AssertionConditionType - { - /// Marked parameter should be evaluated to true. - IS_TRUE = 0, - - /// Marked parameter should be evaluated to false. - IS_FALSE = 1, - - /// Marked parameter should be evaluated to null value. - IS_NULL = 2, - - /// Marked parameter should be evaluated to not null value. - IS_NOT_NULL = 3 - } - - /// - /// Indicates that the marked method unconditionally terminates control flow execution. - /// For example, it could unconditionally throw exception. - /// - [Obsolete("Use [ContractAnnotation('=> halt')] instead")] - [AttributeUsage(AttributeTargets.Method)] - public sealed class TerminatesProgramAttribute : Attribute - { - } - - /// - /// Indicates that method is pure LINQ method, with postponed enumeration (like Enumerable.Select, - /// .Where). This annotation allows inference of [InstantHandle] annotation for parameters - /// of delegate type by analyzing LINQ method chains. - /// - [AttributeUsage(AttributeTargets.Method)] - public sealed class LinqTunnelAttribute : Attribute - { - } - - /// - /// Indicates that IEnumerable passed as a parameter is not enumerated. - /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. - /// - /// - /// - /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class - /// { - /// // custom check for null but no enumeration - /// } - /// - /// void Foo(IEnumerable<string> values) - /// { - /// ThrowIfNull(values, nameof(values)); - /// var x = values.ToList(); // No warnings about multiple enumeration - /// } - /// - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class NoEnumerationAttribute : Attribute - { - } - - /// - /// Indicates that the marked parameter is a regular expression pattern. - /// - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class RegexPatternAttribute : Attribute - { - } - - /// - /// Prevents the Member Reordering feature from tossing members of the marked class. - /// - /// - /// The attribute must be mentioned in your member reordering patterns. - /// - [AttributeUsage( - AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum)] - public sealed class NoReorderAttribute : Attribute - { - } - - /// - /// XAML attribute. Indicates the type that has ItemsSource property and should be treated - /// as ItemsControl-derived type, to enable inner items DataContext type resolve. - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class XamlItemsControlAttribute : Attribute - { - } - - /// - /// XAML attribute. Indicates the property of some BindingBase-derived type, that - /// is used to bind some item of ItemsControl-derived type. This annotation will - /// enable the DataContext type resolve for XAML bindings for such properties. - /// - /// - /// Property should have the tree ancestor of the ItemsControl type or - /// marked with the attribute. - /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class XamlItemBindingOfItemsControlAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class AspChildControlTypeAttribute : Attribute - { - public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) - { - TagName = tagName; - ControlType = controlType; - } - - [NotNull] - public string TagName { get; } - - [NotNull] - public Type ControlType { get; } - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - public sealed class AspDataFieldAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] - public sealed class AspDataFieldsAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property)] - public sealed class AspMethodPropertyAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class AspRequiredAttributeAttribute : Attribute - { - public AspRequiredAttributeAttribute([NotNull] string attribute) - { - Attribute = attribute; - } - - [NotNull] - public string Attribute { get; } - } - - [AttributeUsage(AttributeTargets.Property)] - public sealed class AspTypePropertyAttribute : Attribute - { - public AspTypePropertyAttribute(bool createConstructorReferences) - { - CreateConstructorReferences = createConstructorReferences; - } - - public bool CreateConstructorReferences { get; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class RazorImportNamespaceAttribute : Attribute - { - public RazorImportNamespaceAttribute([NotNull] string name) - { - Name = name; - } - - [NotNull] - public string Name { get; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class RazorInjectionAttribute : Attribute - { - public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) - { - Type = type; - FieldName = fieldName; - } - - [NotNull] - public string Type { get; } - - [NotNull] - public string FieldName { get; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class RazorDirectiveAttribute : Attribute - { - public RazorDirectiveAttribute([NotNull] string directive) - { - Directive = directive; - } - - [NotNull] - public string Directive { get; } - } - - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class RazorPageBaseTypeAttribute : Attribute - { - public RazorPageBaseTypeAttribute([NotNull] string baseType) - { - BaseType = baseType; - } - - public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) - { - BaseType = baseType; - PageName = pageName; - } - - [NotNull] - public string BaseType { get; } - - [CanBeNull] - public string PageName { get; } - } - - [AttributeUsage(AttributeTargets.Method)] - public sealed class RazorHelperCommonAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property)] - public sealed class RazorLayoutAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Method)] - public sealed class RazorWriteLiteralMethodAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Method)] - public sealed class RazorWriteMethodAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Parameter)] - public sealed class RazorWriteMethodParameterAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Properties/AssemblyInfo.cs b/src/Artemis.UI.Shared/Properties/AssemblyInfo.cs deleted file mode 100644 index 412a20daf..000000000 --- a/src/Artemis.UI.Shared/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Runtime.InteropServices; -using System.Windows; - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//In order to begin building localizable applications, set -//CultureYouAreCodingWith in your .csproj file -//inside a . For example, if you are using US english -//in your source files, set the to en-US. Then uncomment -//the NeutralResourceLanguage attribute below. Update the "en-US" in -//the line below to match the UICulture setting in the project file. - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Properties/DesignTimeResources.xaml b/src/Artemis.UI.Shared/Properties/DesignTimeResources.xaml deleted file mode 100644 index dec7a9dd8..000000000 --- a/src/Artemis.UI.Shared/Properties/DesignTimeResources.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Properties/Resources.Designer.cs b/src/Artemis.UI.Shared/Properties/Resources.Designer.cs deleted file mode 100644 index f931adadc..000000000 --- a/src/Artemis.UI.Shared/Properties/Resources.Designer.cs +++ /dev/null @@ -1,62 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Artemis.UI.Shared.Properties { - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if ((resourceMan == null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Artemis.UI.Shared.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/src/Artemis.UI.Shared/Properties/Resources.resx b/src/Artemis.UI.Shared/Properties/Resources.resx deleted file mode 100644 index af7dbebba..000000000 --- a/src/Artemis.UI.Shared/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Properties/Settings.Designer.cs b/src/Artemis.UI.Shared/Properties/Settings.Designer.cs deleted file mode 100644 index d2346eb37..000000000 --- a/src/Artemis.UI.Shared/Properties/Settings.Designer.cs +++ /dev/null @@ -1,30 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Artemis.UI.Shared.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} diff --git a/src/Artemis.UI.Shared/Properties/Settings.settings b/src/Artemis.UI.Shared/Properties/Settings.settings deleted file mode 100644 index c14891b94..000000000 --- a/src/Artemis.UI.Shared/Properties/Settings.settings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/PropertyInput/PropertyInputRegistration.cs b/src/Artemis.UI.Shared/PropertyInput/PropertyInputRegistration.cs deleted file mode 100644 index e118e36c6..000000000 --- a/src/Artemis.UI.Shared/PropertyInput/PropertyInputRegistration.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Artemis.Core; -using Artemis.UI.Shared.Services; - -namespace Artemis.UI.Shared -{ - /// - /// Represents a property input registration registered through - /// - public class PropertyInputRegistration - { - private readonly IProfileEditorService _profileEditorService; - - internal PropertyInputRegistration(IProfileEditorService profileEditorService, Plugin plugin, Type supportedType, Type viewModelType) - { - _profileEditorService = profileEditorService; - Plugin = plugin; - SupportedType = supportedType; - ViewModelType = viewModelType; - - if (Plugin != Constants.CorePlugin) - Plugin.Disabled += InstanceOnDisabled; - } - - /// - /// Gets the plugin that registered the property input - /// - public Plugin Plugin { get; } - - /// - /// Gets the type supported by the property input - /// - public Type SupportedType { get; } - - /// - /// Gets the view model type of the property input - /// - public Type ViewModelType { get; } - - internal void Unsubscribe() - { - if (Plugin != Constants.CorePlugin) - Plugin.Disabled -= InstanceOnDisabled; - } - - private void InstanceOnDisabled(object? sender, EventArgs e) - { - // Profile editor service will call Unsubscribe - _profileEditorService.RemovePropertyInput(this); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs b/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs deleted file mode 100644 index f03156dd0..000000000 --- a/src/Artemis.UI.Shared/PropertyInput/PropertyInputViewModel.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Artemis.Core; -using Artemis.UI.Shared.Services; -using Stylet; - -namespace Artemis.UI.Shared -{ - /// - /// Represents the base class for a property input view model that is used to edit layer properties - /// - /// The type of property this input view model supports - public abstract class PropertyInputViewModel : PropertyInputViewModel - { - private bool _inputDragging; - [AllowNull] - private T _inputValue = default!; - - /// - /// Creates a new instance of the class - /// - /// The layer property this view model will edit - /// The profile editor service - protected PropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService) - { - LayerProperty = layerProperty; - ProfileEditorService = profileEditorService; - } - - /// - /// Creates a new instance of the class - /// - /// The layer property this view model will edit - /// The profile editor service - /// The validator used to validate the input - protected PropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IModelValidator validator) : base(validator) - { - LayerProperty = layerProperty; - ProfileEditorService = profileEditorService; - } - - /// - /// Gets the layer property this view model is editing - /// - public LayerProperty LayerProperty { get; } - - /// - /// Gets a boolean indicating whether the layer property should be enabled - /// - public bool IsEnabled => !LayerProperty.HasDataBinding; - - /// - /// Gets the profile editor service - /// - public IProfileEditorService ProfileEditorService { get; } - - /// - /// Gets or sets a boolean indicating whether the input is currently being dragged - /// - /// Only applicable when using something like a , see - /// and - /// - /// - public bool InputDragging - { - get => _inputDragging; - private set => SetAndNotify(ref _inputDragging, value); - } - - /// - /// Gets or sets the input value - /// - [AllowNull] - public T InputValue - { - get => _inputValue; - set - { - SetAndNotify(ref _inputValue, value); - ApplyInputValue(); - } - } - - internal override object InternalGuard { get; } = new(); - - #region Overrides of Screen - - /// - protected override void OnInitialActivate() - { - LayerProperty.Updated += LayerPropertyOnUpdated; - LayerProperty.CurrentValueSet += LayerPropertyOnUpdated; - LayerProperty.DataBinding.DataBindingEnabled += OnDataBindingChange; - LayerProperty.DataBinding.DataBindingDisabled += OnDataBindingChange; - UpdateInputValue(); - base.OnInitialActivate(); - } - - /// - protected override void OnClose() - { - LayerProperty.Updated -= LayerPropertyOnUpdated; - LayerProperty.CurrentValueSet -= LayerPropertyOnUpdated; - LayerProperty.DataBinding.DataBindingEnabled -= OnDataBindingChange; - LayerProperty.DataBinding.DataBindingDisabled -= OnDataBindingChange; - base.OnClose(); - } - - #endregion - - /// - /// Called when the input value has been applied to the layer property - /// - protected virtual void OnInputValueApplied() - { - } - - /// - /// Called when the input value has changed - /// - protected virtual void OnInputValueChanged() - { - } - - /// - /// Called when data bindings have been enabled or disabled on the layer property - /// - protected virtual void OnDataBindingsChanged() - { - } - - /// - /// Applies the input value to the layer property - /// - protected void ApplyInputValue() - { - // Force the validator to run - if (Validator != null) - Validate(); - // Only apply the input value to the layer property if the validator found no errors - if (!HasErrors) - { - OnInputValueChanged(); - LayerProperty.SetCurrentValue(_inputValue, ProfileEditorService.CurrentTime); - OnInputValueApplied(); - - if (InputDragging) - ProfileEditorService.UpdateProfilePreview(); - else - ProfileEditorService.SaveSelectedProfileElement(); - } - } - - private void UpdateInputValue() - { - // Avoid unnecessary UI updates and validator cycles - if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null) - return; - - // Override the input value - _inputValue = LayerProperty.CurrentValue; - - // Notify a change in the input value - OnInputValueChanged(); - NotifyOfPropertyChange(nameof(InputValue)); - - // Force the validator to run with the overridden value - if (Validator != null) - Validate(); - } - - #region Event handlers - - /// - /// Called by the view input drag has started - /// - /// To use, add the following to DraggableFloat in your xaml: DragStarted="{s:Action InputDragStarted}" - /// - /// - public void InputDragStarted(object sender, EventArgs e) - { - InputDragging = true; - } - - /// - /// Called by the view when input drag has ended - /// - /// To use, add the following to DraggableFloat in your xaml: DragEnded="{s:Action InputDragEnded}" - /// - /// - public void InputDragEnded(object sender, EventArgs e) - { - InputDragging = false; - ProfileEditorService.SaveSelectedProfileElement(); - } - - private void LayerPropertyOnUpdated(object? sender, EventArgs e) - { - UpdateInputValue(); - } - - private void OnDataBindingChange(object? sender, DataBindingEventArgs e) - { - NotifyOfPropertyChange(nameof(IsEnabled)); - OnDataBindingsChanged(); - } - - #endregion - } - - /// - /// For internal use only, implement instead. - /// - public abstract class PropertyInputViewModel : Screen - { - /// - /// For internal use only, implement instead. - /// - protected PropertyInputViewModel() - { - } - - /// - /// For internal use only, implement instead. - /// - protected PropertyInputViewModel(IModelValidator validator) : base(validator) - { - } - - /// - /// Prevents this type being implemented directly, implement instead. - /// - // ReSharper disable once UnusedMember.Global - internal abstract object InternalGuard { get; } - } -} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Providers/IColorGradientStorageProvider.cs b/src/Artemis.UI.Shared/Providers/IColorGradientStorageProvider.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Providers/IColorGradientStorageProvider.cs rename to src/Artemis.UI.Shared/Providers/IColorGradientStorageProvider.cs diff --git a/src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs b/src/Artemis.UI.Shared/Providers/ICursorProvider.cs similarity index 100% rename from src/Avalonia/Artemis.UI.Shared/Providers/ICursorProvider.cs rename to src/Artemis.UI.Shared/Providers/ICursorProvider.cs diff --git a/src/Artemis.UI.Shared/ResourceDictionaries/DataModelConditions.xaml b/src/Artemis.UI.Shared/ResourceDictionaries/DataModelConditions.xaml deleted file mode 100644 index f29058c56..000000000 --- a/src/Artemis.UI.Shared/ResourceDictionaries/DataModelConditions.xaml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Resources/ArtemisShared.xaml b/src/Artemis.UI.Shared/Resources/ArtemisShared.xaml deleted file mode 100644 index 329817f4b..000000000 --- a/src/Artemis.UI.Shared/Resources/ArtemisShared.xaml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Resources/Fonts/RobotoMono-Regular.ttf b/src/Artemis.UI.Shared/Resources/Fonts/RobotoMono-Regular.ttf deleted file mode 100644 index 5919b5d1bf061d687f60300fd7ab774c6b06df50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109212 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 Qit=CXyrr1vyPZG%7iFl@s{jB1 diff --git a/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml b/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml deleted file mode 100644 index a65e58422..000000000 --- a/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionViewModel.cs b/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionViewModel.cs deleted file mode 100644 index 9750c9eb7..000000000 --- a/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionViewModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Windows; -using MaterialDesignThemes.Wpf; -using Stylet; - -namespace Artemis.UI.Shared.Screens.Exceptions -{ - internal class ExceptionViewModel : Screen - { - public ExceptionViewModel(string message, Exception exception) - { - Header = message; - Exception = exception.ToString(); - MessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(2)); - } - - public string Header { get; } - public string Exception { get; } - public SnackbarMessageQueue MessageQueue { get; } - - public void CopyException() - { - Clipboard.SetText(Exception); - MessageQueue.Enqueue("Copied exception to clipboard"); - } - - public void Close() - { - RequestClose(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml deleted file mode 100644 index af5fdcf45..000000000 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs deleted file mode 100644 index 60080f009..000000000 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using Artemis.Core; -using Stylet; - -namespace Artemis.UI.Shared.Screens.GradientEditor -{ - internal class ColorStopViewModel : PropertyChangedBase - { - private readonly GradientEditorViewModel _gradientEditorViewModel; - private bool _isSelected; - private bool _willRemoveColorStop; - - public ColorStopViewModel(GradientEditorViewModel gradientEditorViewModel, ColorGradientStop colorStop) - { - _gradientEditorViewModel = gradientEditorViewModel; - ColorStop = colorStop; - } - - public ColorGradientStop ColorStop { get; } - - public double Offset - { - get => ColorStop.Position * _gradientEditorViewModel.PreviewWidth; - set - { - ColorStop.Position = (float) Math.Round(value / _gradientEditorViewModel.PreviewWidth, 3, MidpointRounding.AwayFromZero); - NotifyOfPropertyChange(nameof(Offset)); - NotifyOfPropertyChange(nameof(OffsetPercent)); - NotifyOfPropertyChange(nameof(OffsetFloat)); - } - } - - public float OffsetPercent - { - get => (float) Math.Round(ColorStop.Position * 100.0, MidpointRounding.AwayFromZero); - set - { - ColorStop.Position = Math.Min(100, Math.Max(0, value)) / 100f; - NotifyOfPropertyChange(nameof(Offset)); - NotifyOfPropertyChange(nameof(OffsetPercent)); - NotifyOfPropertyChange(nameof(OffsetFloat)); - } - } - - // Functionally similar to Offset Percent, but doesn't round on get in order to prevent inconsistencies (and is 0 to 1) - public float OffsetFloat - { - get => ColorStop.Position; - set - { - ColorStop.Position = Math.Min(1, Math.Max(0, value)); - NotifyOfPropertyChange(nameof(Offset)); - NotifyOfPropertyChange(nameof(OffsetPercent)); - NotifyOfPropertyChange(nameof(OffsetFloat)); - } - } - - public bool IsSelected - { - get => _isSelected; - set => SetAndNotify(ref _isSelected, value); - } - - public bool WillRemoveColorStop - { - get => _willRemoveColorStop; - set => SetAndNotify(ref _willRemoveColorStop, value); - } - - #region Movement - - public void StopMouseDown(object sender, MouseButtonEventArgs e) - { - e.Handled = true; - - ((IInputElement) sender).CaptureMouse(); - _gradientEditorViewModel.SelectColorStop(this); - } - - public void StopMouseUp(object sender, MouseButtonEventArgs e) - { - e.Handled = true; - - ((IInputElement) sender).ReleaseMouseCapture(); - if (WillRemoveColorStop) - _gradientEditorViewModel.RemoveColorStop(this); - } - - public void StopMouseMove(object sender, MouseEventArgs e) - { - e.Handled = true; - - if (!((IInputElement) sender).IsMouseCaptured) - return; - - Canvas? parent = VisualTreeUtilities.FindParent((DependencyObject) sender, null); - Point position = e.GetPosition(parent); - if (position.Y > 50) - { - WillRemoveColorStop = true; - return; - } - - WillRemoveColorStop = false; - - double minValue = 0.0; - double maxValue = _gradientEditorViewModel.PreviewWidth; - List stops = _gradientEditorViewModel.ColorGradient.ToList(); - ColorGradientStop? previous = stops.IndexOf(ColorStop) >= 1 ? stops[stops.IndexOf(ColorStop) - 1] : null; - ColorGradientStop? next = stops.IndexOf(ColorStop) + 1 < stops.Count ? stops[stops.IndexOf(ColorStop) + 1] : null; - if (previous != null) - minValue = previous.Position * _gradientEditorViewModel.PreviewWidth; - if (next != null) - maxValue = next.Position * _gradientEditorViewModel.PreviewWidth; - - Offset = Math.Max(minValue, Math.Min(maxValue, position.X)); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml deleted file mode 100644 index ee74e5784..000000000 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Gradient saving not implemented yet - - - Soon you'll be able to store different gradients for usage throughout your profiles and quickly select them - - - - - - Gradient - - - - - - - - - - Clear Gradient? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Selected stop: - - - - - - - - - - - - - - - - - - - - -