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:
parent
a0260b53e5
commit
850346ccd2
4
src/.idea/.idea.Artemis/.idea/avalonia.xml
generated
4
src/.idea/.idea.Artemis/.idea/avalonia.xml
generated
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,8 @@ namespace Artemis.Core.Internal
|
||||
DataBinding = dataBinding;
|
||||
DataBinding.DataBindingPropertiesCleared += DataBindingOnDataBindingPropertiesCleared;
|
||||
DataBinding.DataBindingPropertyRegistered += DataBindingOnDataBindingPropertyRegistered;
|
||||
|
||||
Id = IExitNode.NodeId;
|
||||
|
||||
CreateInputPins();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user