using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Reactive; using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; using Avalonia.Threading; using DynamicData; using DynamicData.List; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase { private readonly INodeEditorService _nodeEditorService; private readonly INodeService _nodeService; private readonly IProfileService _profileService; private readonly ISettingsService _settingsService; private readonly IWindowService _windowService; public NodeScriptWindowViewModel(NodeScript nodeScript, INodeService nodeService, INodeEditorService nodeEditorService, INodeVmFactory vmFactory, ISettingsService settingsService, IProfileService profileService, IWindowService windowService) : base(nodeScript) { 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; _profileService = profileService; _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); this.WhenActivated(d => { DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(25.0 / 1000), DispatcherPriority.Normal, Update); // TODO: Remove in favor of saving each time a node editor command is executed DispatcherTimer saveTimer = new(TimeSpan.FromMinutes(2), DispatcherPriority.Normal, Save); updateTimer.Start(); saveTimer.Start(); Disposable.Create(() => { updateTimer.Stop(); saveTimer.Stop(); }).DisposeWith(d); }); } 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", true); 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(); } private void Update(object? sender, EventArgs e) { NodeScript.Run(); } private void Save(object? sender, EventArgs e) { if (NodeScript.Context is Profile profile) _profileService.SaveProfile(profile, true); } }