From 850346ccd2f61b91f34f2af56785062148b69ef1 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 22 May 2022 22:38:57 +0200 Subject: [PATCH] Node editor - Added menu Node scripts - Fixed IDs not being set for regular nodes Enums node - Hardened VM logic against weird connections --- src/.idea/.idea.Artemis/.idea/avalonia.xml | 4 + .../Profile/Conditions/EventCondition.cs | 2 +- src/Artemis.Core/Services/NodeService.cs | 20 +++ .../Internal/DataBindingExitNode.cs | 3 +- src/Artemis.Core/VisualScripting/Node.cs | 8 +- .../VisualScripting/NodeScript.cs | 17 +- .../ConditionTypes/EventConditionViewModel.cs | 14 +- .../StaticConditionViewModel.cs | 2 +- .../Panels/MenuBar/MenuBarViewModel.cs | 29 +--- .../VisualScripting/NodeScriptView.axaml.cs | 9 + .../VisualScripting/NodeScriptViewModel.cs | 14 ++ .../NodeScriptWindowView.axaml | 157 +++++++++++++++++- .../NodeScriptWindowViewModel.cs | 126 ++++++++++++-- .../Screens/EnumEqualsNodeCustomViewModel.cs | 3 +- 14 files changed, 352 insertions(+), 56 deletions(-) diff --git a/src/.idea/.idea.Artemis/.idea/avalonia.xml b/src/.idea/.idea.Artemis/.idea/avalonia.xml index b11284c50..07f14ce84 100644 --- a/src/.idea/.idea.Artemis/.idea/avalonia.xml +++ b/src/.idea/.idea.Artemis/.idea/avalonia.xml @@ -41,6 +41,7 @@ + @@ -52,6 +53,9 @@ + + + diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index 26c262cd0..1942aa169 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -55,7 +55,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition public NodeScript Script { get => _script; - set => SetAndNotify(ref _script, value); + private set => SetAndNotify(ref _script, value); } /// diff --git a/src/Artemis.Core/Services/NodeService.cs b/src/Artemis.Core/Services/NodeService.cs index c0482363a..70c7d2e9b 100644 --- a/src/Artemis.Core/Services/NodeService.cs +++ b/src/Artemis.Core/Services/NodeService.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Reflection; using System.Security.Cryptography; using System.Text; +using Artemis.Core.VisualScripting; using Artemis.Storage.Entities.Profile.Nodes; +using Newtonsoft.Json; using Ninject; using SkiaSharp; @@ -80,6 +82,21 @@ namespace Artemis.Core.Services return NodeTypeStore.AddColor(type, color, plugin); } + public string ExportScript(NodeScript nodeScript) + { + nodeScript.Save(); + return JsonConvert.SerializeObject(nodeScript.Entity, IProfileService.ExportSettings); + } + + public void ImportScript(string json, NodeScript target) + { + NodeScriptEntity? entity = JsonConvert.DeserializeObject(json); + if (entity == null) + throw new ArtemisCoreException("Failed to load node script"); + + target.LoadFromEntity(entity); + } + 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"); @@ -135,5 +152,8 @@ namespace Artemis.Core.Services /// The type to associate the color with /// The color to display TypeColorRegistration RegisterTypeColor(Plugin plugin, Type type, SKColor color); + + string ExportScript(NodeScript nodeScript); + void ImportScript(string json, NodeScript target); } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs index 2ca393ca6..b47ff5a07 100644 --- a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs @@ -16,7 +16,8 @@ namespace Artemis.Core.Internal DataBinding = dataBinding; DataBinding.DataBindingPropertiesCleared += DataBindingOnDataBindingPropertiesCleared; DataBinding.DataBindingPropertyRegistered += DataBindingOnDataBindingPropertyRegistered; - + Id = IExitNode.NodeId; + CreateInputPins(); } diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index 70804e43e..a7f155e17 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -15,14 +15,14 @@ public abstract class Node : CorePropertyChanged, INode public event EventHandler? Resetting; #region Properties & Fields - + private Guid _id; /// public Guid Id { get => _id; - set => SetAndNotify(ref _id , value); + set => SetAndNotify(ref _id, value); } private string _name; @@ -73,7 +73,7 @@ public abstract class Node : CorePropertyChanged, INode public IReadOnlyCollection Pins => new ReadOnlyCollection(_pins); private readonly List _pinCollections = new(); - + /// public IReadOnlyCollection PinCollections => new ReadOnlyCollection(_pinCollections); @@ -88,6 +88,7 @@ public abstract class Node : CorePropertyChanged, INode { _name = string.Empty; _description = string.Empty; + _id = Guid.NewGuid(); } /// @@ -97,6 +98,7 @@ public abstract class Node : CorePropertyChanged, INode { _name = name; _description = description; + _id = Guid.NewGuid(); } #endregion diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 22909973f..dc7b4e0cf 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -29,7 +29,7 @@ namespace Artemis.Core #region Properties & Fields - internal NodeScriptEntity Entity { get; } + internal NodeScriptEntity Entity { get; private set; } /// public string Name { get; } @@ -183,8 +183,15 @@ namespace Artemis.Core LoadConnections(); } + internal void LoadFromEntity(NodeScriptEntity entity) + { + Entity = entity; + Load(); + } + private void LoadExistingNode(INode node, NodeEntity nodeEntity) { + node.Id = nodeEntity.Id; node.X = nodeEntity.X; node.Y = nodeEntity.Y; @@ -245,17 +252,21 @@ namespace Artemis.Core // Clear existing connections on input pins, we don't want none of that now if (targetPin.Direction == PinDirection.Input) + { while (targetPin.ConnectedTo.Any()) targetPin.DisconnectFrom(targetPin.ConnectedTo[0]); + } if (sourcePin.Direction == PinDirection.Input) + { while (sourcePin.ConnectedTo.Any()) sourcePin.DisconnectFrom(sourcePin.ConnectedTo[0]); + } // Only connect the nodes if they aren't already connected (LoadConnections may be called twice or more) - if (!targetPin.ConnectedTo.Contains(sourcePin)) + if (!targetPin.ConnectedTo.Contains(sourcePin) && targetPin.IsTypeCompatible(sourcePin.Type)) targetPin.ConnectTo(sourcePin); - if (!sourcePin.ConnectedTo.Contains(targetPin)) + if (!sourcePin.ConnectedTo.Contains(targetPin) && sourcePin.IsTypeCompatible(targetPin.Type)) sourcePin.ConnectTo(targetPin); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs index b8d5a546d..858dadc82 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/EventConditionViewModel.cs @@ -16,6 +16,7 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes; public class EventConditionViewModel : ActivatableViewModelBase { private readonly EventCondition _eventCondition; + private readonly INodeService _nodeService; private readonly IProfileEditorService _profileEditorService; private readonly ISettingsService _settingsService; private readonly ObservableAsPropertyHelper _showOverlapOptions; @@ -26,18 +27,15 @@ public class EventConditionViewModel : ActivatableViewModelBase private ObservableAsPropertyHelper? _selectedToggleOffMode; private ObservableAsPropertyHelper? _selectedTriggerMode; - public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, IWindowService windowService, ISettingsService settingsService) + public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, INodeService nodeService, IWindowService windowService, ISettingsService settingsService) { _eventCondition = eventCondition; _profileEditorService = profileEditorService; + _nodeService = nodeService; _windowService = windowService; _settingsService = settingsService; - _showOverlapOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode) - .Select(m => m == 0) - .ToProperty(this, vm => vm.ShowOverlapOptions); - _showToggleOffOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode) - .Select(m => m == 1) - .ToProperty(this, vm => vm.ShowToggleOffOptions); + _showOverlapOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode).Select(m => m == 0).ToProperty(this, vm => vm.ShowOverlapOptions); + _showToggleOffOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode).Select(m => m == 1).ToProperty(this, vm => vm.ShowToggleOffOptions); this.WhenActivated(d => { @@ -82,6 +80,6 @@ public class EventConditionViewModel : ActivatableViewModelBase private async Task ExecuteOpenEditor() { - await _windowService.ShowDialogAsync(("nodeScript", _eventCondition.NodeScript)); + await _windowService.ShowDialogAsync(("nodeScript", _eventCondition.Script)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs index c6e7dadd7..971ab5b83 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/DisplayCondition/ConditionTypes/StaticConditionViewModel.cs @@ -51,6 +51,6 @@ public class StaticConditionViewModel : ActivatableViewModelBase private async Task ExecuteOpenEditor() { - await _windowService.ShowDialogAsync(("nodeScript", _staticCondition.NodeScript)); + await _windowService.ShowDialogAsync(("nodeScript", _staticCondition.Script)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 31589458d..54cb1e197 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -12,7 +12,6 @@ using Artemis.UI.Screens.Sidebar; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; -using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Newtonsoft.Json; using ReactiveUI; using Serilog; @@ -27,10 +26,10 @@ public class MenuBarViewModel : ActivatableViewModelBase private readonly ISettingsService _settingsService; private readonly IWindowService _windowService; private ProfileEditorHistory? _history; - private ObservableAsPropertyHelper? _suspendedEditing; private ObservableAsPropertyHelper? _isSuspended; private ObservableAsPropertyHelper? _profileConfiguration; private ObservableAsPropertyHelper? _profileElement; + private ObservableAsPropertyHelper? _suspendedEditing; public MenuBarViewModel(ILogger logger, IProfileEditorService profileEditorService, IProfileService profileService, ISettingsService settingsService, IWindowService windowService) { @@ -62,26 +61,26 @@ public class MenuBarViewModel : ActivatableViewModelBase DuplicateProfile = ReactiveCommand.Create(ExecuteDuplicateProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); ToggleSuspendedEditing = ReactiveCommand.Create(ExecuteToggleSuspendedEditing); ToggleBooleanSetting = ReactiveCommand.Create>(ExecuteToggleBooleanSetting); - OpenUri = ReactiveCommand.CreateFromTask(ExecuteOpenUri); + OpenUri = ReactiveCommand.Create(s => Process.Start(new ProcessStartInfo(s) {UseShellExecute = true, Verb = "open"})); } public ReactiveCommand AddFolder { get; } public ReactiveCommand AddLayer { get; } public ReactiveCommand ToggleSuspended { get; } public ReactiveCommand ViewProperties { get; } - public ReactiveCommand AdaptProfile { get; } + public ReactiveCommand AdaptProfile { get; } public ReactiveCommand DeleteProfile { get; } public ReactiveCommand ExportProfile { get; } public ReactiveCommand DuplicateProfile { get; } public ReactiveCommand, Unit> ToggleBooleanSetting { get; } public ReactiveCommand OpenUri { get; } public ReactiveCommand ToggleSuspendedEditing { get; } - + public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public RenderProfileElement? ProfileElement => _profileElement?.Value; public bool IsSuspended => _isSuspended?.Value ?? false; public bool SuspendedEditing => _suspendedEditing?.Value ?? false; - + public PluginSetting AutoSuspend => _settingsService.GetSetting("ProfileEditor.AutoSuspend", true); public PluginSetting FocusSelectedLayer => _settingsService.GetSetting("ProfileEditor.FocusSelectedLayer", false); public PluginSetting ShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); @@ -123,6 +122,7 @@ public class MenuBarViewModel : ActivatableViewModelBase ("profileConfiguration", ProfileConfiguration) ); } + private async Task ExecuteAdaptProfile() { if (ProfileConfiguration?.Profile == null) @@ -190,28 +190,15 @@ public class MenuBarViewModel : ActivatableViewModelBase ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); _profileService.ImportProfile(ProfileConfiguration.Category, export, true, false, "copy"); } - + private void ExecuteToggleSuspendedEditing() { _profileEditorService.ChangeSuspendedEditing(!SuspendedEditing); } - + private void ExecuteToggleBooleanSetting(PluginSetting setting) { setting.Value = !setting.Value; setting.Save(); } - - private async Task ExecuteOpenUri(string uri) - { - try - { - Process.Start(new ProcessStartInfo(uri) {UseShellExecute = true, Verb = "open"}); - } - catch (Exception e) - { - _logger.Error(e, "Failed to open URL"); - await _windowService.ShowConfirmContentDialog("Failed to open URL", "We couldn't open the URL for you, check the logs for more details", "Confirm", null); - } - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index ea59272a8..f87985bc2 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -45,6 +45,7 @@ public class NodeScriptView : ReactiveUserControl _zoomBorder.AddHandler(PointerWheelChangedEvent, ZoomOnPointerWheelChanged, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); this.WhenActivated(d => { + ViewModel.AutoFitRequested += ViewModelOnAutoFitRequested; ViewModel!.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d); if (ViewModel.IsPreview) { @@ -53,6 +54,7 @@ public class NodeScriptView : ReactiveUserControl } Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle); + Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); }); } @@ -117,6 +119,11 @@ public class NodeScriptView : ReactiveUserControl _zoomBorder.Pan(Bounds.Center.X - scriptRect.Center.X * scale, Bounds.Center.Y - scriptRect.Center.Y * scale, skipTransitions); } + private void ViewModelOnAutoFitRequested(object? sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => AutoFit(false)); + } + private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (e.Property.Name == nameof(_zoomBorder.Background)) @@ -136,6 +143,8 @@ public class NodeScriptView : ReactiveUserControl private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e) { + if (ViewModel != null) + ViewModel.PanMatrix = _zoomBorder.Matrix; UpdateZoomBorderBackground(); } diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 09ff9440b..e73c87cb6 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -30,6 +30,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase private DragCableViewModel? _dragViewModel; private List? _initialNodeSelection; + private Matrix _panMatrix; public NodeScriptViewModel(NodeScript nodeScript, bool isPreview, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService) { @@ -95,6 +96,12 @@ public class NodeScriptViewModel : ActivatableViewModelBase get => _dragViewModel; set => RaiseAndSetIfChanged(ref _dragViewModel, value); } + + public Matrix PanMatrix + { + get => _panMatrix; + set => RaiseAndSetIfChanged(ref _panMatrix, value); + } public void DeleteSelectedNodes() { @@ -223,4 +230,11 @@ public class NodeScriptViewModel : ActivatableViewModelBase if (toRemove != null) _nodeViewModels.Remove(toRemove); } + + public void RequestAutoFit() + { + AutoFitRequested?.Invoke(this, EventArgs.Empty); + } + + public event EventHandler? AutoFitRequested; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml index 55685bd32..8b9a2c801 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml @@ -4,18 +4,163 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptWindowView" x:DataType="visualScripting:NodeScriptWindowViewModel" Title="Artemis | Visual Script Editor" Width="1200" Height="800"> - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs index 422cef7cf..e0dabe1da 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs @@ -1,18 +1,122 @@ -using Artemis.Core; +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Reactive; +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; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; +using Avalonia; +using DynamicData; +using DynamicData.List; +using ReactiveUI; -namespace Artemis.UI.Screens.VisualScripting +namespace Artemis.UI.Screens.VisualScripting; + +public class NodeScriptWindowViewModel : DialogViewModelBase { - public class NodeScriptWindowViewModel : DialogViewModelBase - { - public NodeScript NodeScript { get; } - public NodeScriptViewModel NodeScriptViewModel { get; set; } + private readonly INodeService _nodeService; + private readonly INodeEditorService _nodeEditorService; + private readonly ISettingsService _settingsService; + private readonly IWindowService _windowService; - public NodeScriptWindowViewModel(NodeScript nodeScript, INodeVmFactory vmFactory) - { - NodeScript = nodeScript; - NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false); - } + public NodeScriptWindowViewModel(NodeScript nodeScript, + INodeService nodeService, + INodeEditorService nodeEditorService, + INodeVmFactory vmFactory, + ISettingsService settingsService, + IWindowService windowService) + { + NodeScript = nodeScript; + NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false); + OpenUri = ReactiveCommand.Create(s => Process.Start(new ProcessStartInfo(s) {UseShellExecute = true, Verb = "open"})); + ToggleBooleanSetting = ReactiveCommand.Create>(ExecuteToggleBooleanSetting); + History = nodeEditorService.GetHistory(nodeScript); + + _nodeService = nodeService; + _nodeEditorService = nodeEditorService; + _settingsService = settingsService; + _windowService = windowService; + + SourceList nodeSourceList = new(); + nodeSourceList.AddRange(nodeService.AvailableNodes); + nodeSourceList.Connect() + .GroupWithImmutableState(n => n.Category) + .Bind(out ReadOnlyObservableCollection> categories) + .Subscribe(); + Categories = categories; + + CreateNode = ReactiveCommand.Create(ExecuteCreateNode); + Export = ReactiveCommand.CreateFromTask(ExecuteExport); + Import = ReactiveCommand.CreateFromTask(ExecuteImport); + } + + public NodeScript NodeScript { get; } + public NodeScriptViewModel NodeScriptViewModel { get; set; } + + public NodeEditorHistory History { get; } + public ReactiveCommand, Unit> ToggleBooleanSetting { get; set; } + public ReactiveCommand OpenUri { get; set; } + public ReadOnlyObservableCollection> Categories { get; } + public ReactiveCommand CreateNode { get; } + public ReactiveCommand Export { get; } + public ReactiveCommand Import { get; } + + public PluginSetting ShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + public PluginSetting ShowFullPaths => _settingsService.GetSetting("ProfileEditor.ShowFullPaths", false); + public PluginSetting AlwaysShowValues => _settingsService.GetSetting("ProfileEditor.AlwaysShowValues", false); + + private void ExecuteToggleBooleanSetting(PluginSetting setting) + { + setting.Value = !setting.Value; + setting.Save(); + } + + private void ExecuteCreateNode(NodeData data) + { + // Place the node in the top-left of the canvas, keeping in mind the user may have panned + INode node = data.CreateNode(NodeScript, null); + Point point = new Point(0, 0).Transform(NodeScriptViewModel.PanMatrix.Invert()); + + node.X = Math.Round(point.X / 10d, 0, MidpointRounding.AwayFromZero) * 10d + 20; + node.Y = Math.Round(point.Y / 10d, 0, MidpointRounding.AwayFromZero) * 10d + 20; + + _nodeEditorService.ExecuteCommand(NodeScript, new AddNode(NodeScript, node)); + } + + private async Task ExecuteExport() + { + // Might not cover everything but then the dialog will complain and that's good enough + string? result = await _windowService.CreateSaveFileDialog() + .HavingFilter(f => f.WithExtension("json").WithName("Artemis node script")) + .ShowAsync(); + + if (result == null) + return; + + string json = _nodeService.ExportScript(NodeScript); + await File.WriteAllTextAsync(result, json); + } + + private async Task ExecuteImport() + { + string[]? result = await _windowService.CreateOpenFileDialog() + .HavingFilter(f => f.WithExtension("json").WithName("Artemis node script")) + .ShowAsync(); + + if (result == null) + return; + + string json = await File.ReadAllTextAsync(result[0]); + _nodeService.ImportScript(json, NodeScript); + History.Clear(); + + await Task.Delay(200); + NodeScriptViewModel.RequestAutoFit(); } } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Nodes/Operators/Screens/EnumEqualsNodeCustomViewModel.cs b/src/Artemis.VisualScripting/Nodes/Operators/Screens/EnumEqualsNodeCustomViewModel.cs index 469506c50..16fad79e2 100644 --- a/src/Artemis.VisualScripting/Nodes/Operators/Screens/EnumEqualsNodeCustomViewModel.cs +++ b/src/Artemis.VisualScripting/Nodes/Operators/Screens/EnumEqualsNodeCustomViewModel.cs @@ -27,7 +27,8 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel if (_node.InputPin.ConnectedTo.Any()) { EnumValues.Clear(); - EnumValues.AddRange(Enum.GetValues(_node.InputPin.ConnectedTo.First().Type).Cast()); + if (_node.InputPin.ConnectedTo.First().Type.IsEnum) + EnumValues.AddRange(Enum.GetValues(_node.InputPin.ConnectedTo.First().Type).Cast()); this.RaisePropertyChanged(nameof(CurrentValue)); }