1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Node editor - Added menu

Node scripts - Fixed IDs not being set for regular nodes
Enums node - Hardened VM logic against weird connections
This commit is contained in:
Robert 2022-05-22 22:38:57 +02:00
parent a0260b53e5
commit 850346ccd2
14 changed files with 352 additions and 56 deletions

View File

@ -41,6 +41,7 @@
<entry key="Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/TreePropertyView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml" value="Artemis.UI.Windows/Artemis.UI.Windows.csproj" />
<entry key="Artemis.UI/Screens/ProfileEditor/ProfileEditorTitleBarView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/ProfileEditor/ProfileEditorView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/Root/SplashView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
@ -52,6 +53,9 @@
<entry key="Artemis.UI/Screens/SurfaceEditor/ListDeviceView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/SurfaceEditor/SurfaceDeviceView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/VisualScripting/NodePickerView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml" value="Artemis.UI.Windows/Artemis.UI.Windows.csproj" />
<entry key="Artemis.UI/Screens/VisualScripting/NodeView.axaml" value="Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Avalonia/Artemis.UI/Screens/Debugger/Tabs/Render/RenderDebugView.axaml" value="Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj" />
<entry key="Avalonia/Artemis.UI/Styles/Artemis.axaml" value="Avalonia/Artemis.UI.Linux/Artemis.UI.Linux.csproj" />

View File

@ -55,7 +55,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
public NodeScript<bool> Script
{
get => _script;
set => SetAndNotify(ref _script, value);
private set => SetAndNotify(ref _script, value);
}
/// <summary>

View File

@ -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<NodeScriptEntity>(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
/// <param name="type">The type to associate the color with</param>
/// <param name="color">The color to display</param>
TypeColorRegistration RegisterTypeColor(Plugin plugin, Type type, SKColor color);
string ExportScript(NodeScript nodeScript);
void ImportScript(string json, NodeScript target);
}
}

View File

@ -16,7 +16,8 @@ namespace Artemis.Core.Internal
DataBinding = dataBinding;
DataBinding.DataBindingPropertiesCleared += DataBindingOnDataBindingPropertiesCleared;
DataBinding.DataBindingPropertyRegistered += DataBindingOnDataBindingPropertyRegistered;
Id = IExitNode.NodeId;
CreateInputPins();
}

View File

@ -15,14 +15,14 @@ public abstract class Node : CorePropertyChanged, INode
public event EventHandler? Resetting;
#region Properties & Fields
private Guid _id;
/// <inheritdoc />
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<IPin> Pins => new ReadOnlyCollection<IPin>(_pins);
private readonly List<IPinCollection> _pinCollections = new();
/// <inheritdoc />
public IReadOnlyCollection<IPinCollection> PinCollections => new ReadOnlyCollection<IPinCollection>(_pinCollections);
@ -88,6 +88,7 @@ public abstract class Node : CorePropertyChanged, INode
{
_name = string.Empty;
_description = string.Empty;
_id = Guid.NewGuid();
}
/// <summary>
@ -97,6 +98,7 @@ public abstract class Node : CorePropertyChanged, INode
{
_name = name;
_description = description;
_id = Guid.NewGuid();
}
#endregion

View File

@ -29,7 +29,7 @@ namespace Artemis.Core
#region Properties & Fields
internal NodeScriptEntity Entity { get; }
internal NodeScriptEntity Entity { get; private set; }
/// <inheritdoc />
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);
}
}

View File

@ -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<bool> _showOverlapOptions;
@ -26,18 +27,15 @@ public class EventConditionViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<int>? _selectedToggleOffMode;
private ObservableAsPropertyHelper<int>? _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<NodeScriptWindowViewModel, bool>(("nodeScript", _eventCondition.NodeScript));
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", _eventCondition.Script));
}
}

View File

@ -51,6 +51,6 @@ public class StaticConditionViewModel : ActivatableViewModelBase
private async Task ExecuteOpenEditor()
{
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", _staticCondition.NodeScript));
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", _staticCondition.Script));
}
}

View File

@ -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<bool>? _suspendedEditing;
private ObservableAsPropertyHelper<bool>? _isSuspended;
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<bool>? _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<PluginSetting<bool>>(ExecuteToggleBooleanSetting);
OpenUri = ReactiveCommand.CreateFromTask<string>(ExecuteOpenUri);
OpenUri = ReactiveCommand.Create<string>(s => Process.Start(new ProcessStartInfo(s) {UseShellExecute = true, Verb = "open"}));
}
public ReactiveCommand<Unit, Unit> AddFolder { get; }
public ReactiveCommand<Unit, Unit> AddLayer { get; }
public ReactiveCommand<Unit, Unit> ToggleSuspended { get; }
public ReactiveCommand<Unit, Unit> ViewProperties { get; }
public ReactiveCommand<Unit,Unit> AdaptProfile { get; }
public ReactiveCommand<Unit, Unit> AdaptProfile { get; }
public ReactiveCommand<Unit, Unit> DeleteProfile { get; }
public ReactiveCommand<Unit, Unit> ExportProfile { get; }
public ReactiveCommand<Unit, Unit> DuplicateProfile { get; }
public ReactiveCommand<PluginSetting<bool>, Unit> ToggleBooleanSetting { get; }
public ReactiveCommand<string, Unit> OpenUri { get; }
public ReactiveCommand<Unit, Unit> 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<bool> AutoSuspend => _settingsService.GetSetting("ProfileEditor.AutoSuspend", true);
public PluginSetting<bool> FocusSelectedLayer => _settingsService.GetSetting("ProfileEditor.FocusSelectedLayer", false);
public PluginSetting<bool> 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<bool> 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);
}
}
}

View File

@ -45,6 +45,7 @@ public class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
_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<NodeScriptViewModel>
}
Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle);
Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d);
});
}
@ -117,6 +119,11 @@ public class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
_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<NodeScriptViewModel>
private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e)
{
if (ViewModel != null)
ViewModel.PanMatrix = _zoomBorder.Matrix;
UpdateZoomBorderBackground();
}

View File

@ -30,6 +30,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase
private DragCableViewModel? _dragViewModel;
private List<NodeViewModel>? _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;
}

View File

@ -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">
<Grid Margin="15" ColumnDefinitions="*,*" RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<TextBlock Classes="h4" Text="{CompiledBinding NodeScript.Name}"/>
<TextBlock Classes="subtitle" Margin="0 0 0 10" Text="{CompiledBinding NodeScript.Description}"/>
<Window.KeyBindings>
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z" />
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y" />
</Window.KeyBindings>
<Grid Margin="15" ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,*">
<Menu Grid.Row="0" Grid.ColumnSpan="2" VerticalAlignment="Top" Margin="-10 -10 -10 0">
<MenuItem Header="_File">
<MenuItem Header="Add Node" Items="{CompiledBinding Categories}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Plus" />
</MenuItem.Icon>
<MenuItem.Styles>
<Style Selector="MenuItem > MenuItem > MenuItem">
<Setter Property="Command" Value="{Binding $parent[visualScripting:NodeScriptWindowView].DataContext.CreateNode}" />
<Setter Property="CommandParameter" Value="{Binding}" />
<Setter Property="Items" Value="{Binding Items}" />
</Style>
</MenuItem.Styles>
<MenuItem.DataTemplates>
<DataTemplate DataType="{x:Type core:NodeData}">
<StackPanel Background="Transparent">
<TextBlock Text="{Binding Name}" TextWrapping="Wrap"></TextBlock>
<TextBlock Foreground="{DynamicResource TextFillColorSecondary}" Text="{Binding Description}" TextWrapping="Wrap"></TextBlock>
</StackPanel>
</DataTemplate>
<DataTemplate>
<TextBlock Text="{Binding Key}"></TextBlock>
</DataTemplate>
</MenuItem.DataTemplates>
</MenuItem>
<Separator />
<MenuItem Header="Export Script" Command="{CompiledBinding Export}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Export" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Import Script" Command="{CompiledBinding Import}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Import" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Undo" Command="{CompiledBinding History.Undo}" InputGesture="Ctrl+Z">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Undo" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Redo" Command="{CompiledBinding History.Redo}" InputGesture="Ctrl+Y">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Redo" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="_Duplicate" Command="{Binding Duplicate}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Copy" Command="{Binding Copy}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Paste" Command="{Binding Paste}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIconExt Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Options">
<MenuItem Header="Display Data Model Values"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding ShowDataModelValues}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding ShowDataModelValues.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Full Condition Paths"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding ShowFullPaths}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding ShowFullPaths.Value}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Always Display Cable Values"
ToolTip.Tip="If enabled, cable values are always shown instead of only on hover"
Command="{CompiledBinding ToggleBooleanSetting}"
CommandParameter="{CompiledBinding AlwaysShowValues}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding AlwaysShowValues.Value}" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="Artemis Wiki" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="BookEdit" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Editor" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/en/guides/user/profiles">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Edit" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Layers" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/layers">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Layers" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Display Conditions" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/conditions">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="NotEqual" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Timeline" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/timeline">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Stopwatch" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Data Bindings" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/data-bindings">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="VectorLink" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Scripting" Command="{CompiledBinding OpenUri}" CommandParameter="https://wiki.artemis-rgb.com/guides/user/profiles/scripting">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="CodeJson" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Report a Bug" Command="{CompiledBinding OpenUri}" CommandParameter="https://github.com/Artemis-RGB/Artemis/issues">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Github" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Get Help on Discord" Command="{CompiledBinding OpenUri}" CommandParameter="https://discord.gg/S3MVaC9">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Discord" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal">
<TextBlock Classes="h4" Text="{CompiledBinding NodeScript.Name}" />
<TextBlock Classes="subtitle" Margin="10 0 0 13" Text="{CompiledBinding NodeScript.Description}" VerticalAlignment="Bottom" />
</StackPanel>
<controls:HyperlinkButton Grid.Row="0"
<controls:HyperlinkButton Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Top"
HorizontalAlignment="Right"
@ -23,7 +168,7 @@
Learn more about visual scripts
</controls:HyperlinkButton>
<Border Classes="card" Grid.Row="1" Grid.ColumnSpan="2">
<Border Classes="card-condensed" Grid.Row="2" Grid.ColumnSpan="2">
<ContentControl Content="{CompiledBinding NodeScriptViewModel}" />
</Border>

View File

@ -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<bool>
{
public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
{
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<string>(s => Process.Start(new ProcessStartInfo(s) {UseShellExecute = true, Verb = "open"}));
ToggleBooleanSetting = ReactiveCommand.Create<PluginSetting<bool>>(ExecuteToggleBooleanSetting);
History = nodeEditorService.GetHistory(nodeScript);
_nodeService = nodeService;
_nodeEditorService = nodeEditorService;
_settingsService = settingsService;
_windowService = windowService;
SourceList<NodeData> nodeSourceList = new();
nodeSourceList.AddRange(nodeService.AvailableNodes);
nodeSourceList.Connect()
.GroupWithImmutableState(n => n.Category)
.Bind(out ReadOnlyObservableCollection<IGrouping<NodeData, string>> categories)
.Subscribe();
Categories = categories;
CreateNode = ReactiveCommand.Create<NodeData>(ExecuteCreateNode);
Export = ReactiveCommand.CreateFromTask(ExecuteExport);
Import = ReactiveCommand.CreateFromTask(ExecuteImport);
}
public NodeScript NodeScript { get; }
public NodeScriptViewModel NodeScriptViewModel { get; set; }
public NodeEditorHistory History { get; }
public ReactiveCommand<PluginSetting<bool>, Unit> ToggleBooleanSetting { get; set; }
public ReactiveCommand<string, Unit> OpenUri { get; set; }
public ReadOnlyObservableCollection<IGrouping<NodeData, string>> Categories { get; }
public ReactiveCommand<NodeData, Unit> CreateNode { get; }
public ReactiveCommand<Unit, Unit> Export { get; }
public ReactiveCommand<Unit, Unit> Import { get; }
public PluginSetting<bool> ShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
public PluginSetting<bool> ShowFullPaths => _settingsService.GetSetting("ProfileEditor.ShowFullPaths", false);
public PluginSetting<bool> AlwaysShowValues => _settingsService.GetSetting("ProfileEditor.AlwaysShowValues", false);
private void ExecuteToggleBooleanSetting(PluginSetting<bool> 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();
}
}

View File

@ -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<Enum>());
if (_node.InputPin.ConnectedTo.First().Type.IsEnum)
EnumValues.AddRange(Enum.GetValues(_node.InputPin.ConnectedTo.First().Type).Cast<Enum>());
this.RaisePropertyChanged(nameof(CurrentValue));
}