From 0b5866a2b84d757a2b8f672de75100a7ddce1372 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Fri, 14 Apr 2023 18:48:41 +0100 Subject: [PATCH 1/7] CI - Fixed DocFX again --- docfx/docfx_project/docfx.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docfx/docfx_project/docfx.json b/docfx/docfx_project/docfx.json index dc1d65129..525c27ace 100644 --- a/docfx/docfx_project/docfx.json +++ b/docfx/docfx_project/docfx.json @@ -4,8 +4,8 @@ "src": [ { "files": [ - "Artemis.Core/bin/net6.0/Artemis.Core.dll", - "Artemis.UI.Shared/bin/net6.0/Artemis.UI.Shared.dll", + "Artemis.Core/bin/net7.0/Artemis.Core.dll", + "Artemis.UI.Shared/bin/net7.0/Artemis.UI.Shared.dll", ], "src": "../../src" } From 0c44e4ba22a9ec4711982a5dac9ee2ec4c17b12a Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sun, 16 Apr 2023 17:35:43 +0100 Subject: [PATCH 2/7] Core - Added plugin hot reloading --- .../Services/PluginManagementService.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index c03aec7bb..1e24feb47 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Artemis.Core.DeviceProviders; using Artemis.Core.DryIoc; @@ -30,6 +31,7 @@ internal class PluginManagementService : IPluginManagementService private readonly IPluginRepository _pluginRepository; private readonly List _plugins; private readonly IQueuedActionRepository _queuedActionRepository; + private FileSystemWatcher _hotReloadWatcher; private bool _disposed; private bool _isElevated; @@ -43,6 +45,8 @@ internal class PluginManagementService : IPluginManagementService _plugins = new List(); ProcessPluginDeletionQueue(); + + StartHotReload(); } private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) @@ -218,6 +222,14 @@ internal class PluginManagementService : IPluginManagementService return null; } + private Plugin? GetPluginByDirectory(DirectoryInfo directory) + { + lock (_plugins) + { + return _plugins.FirstOrDefault(p => p.Directory.FullName == directory.FullName); + } + } + public void Dispose() { // Disposal happens manually before container disposal but the container doesn't know that so a 2nd call will be made @@ -912,6 +924,67 @@ internal class PluginManagementService : IPluginManagementService } #endregion + + #region Hot Reload + + private void StartHotReload() + { + // Watch for changes in the plugin directory, "plugin.json". + // If this file is changed, reload the plugin. + _hotReloadWatcher = new FileSystemWatcher(Constants.PluginsFolder, "plugin.json"); + _hotReloadWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.FileName; + _hotReloadWatcher.Created += FileSystemWatcherOnCreated; + _hotReloadWatcher.Error += FileSystemWatcherOnError; + _hotReloadWatcher.IncludeSubdirectories = true; + _hotReloadWatcher.EnableRaisingEvents = true; + } + + private void FileSystemWatcherOnError(object sender, ErrorEventArgs e) + { + _logger.Error(e.GetException(), "File system watcher error"); + } + + private void FileSystemWatcherOnCreated(object sender, FileSystemEventArgs e) + { + string? pluginPath = Path.GetDirectoryName(e.FullPath); + if (pluginPath == null) + { + _logger.Warning("Plugin change detected, but could not get plugin directory. {fullPath}", e.FullPath); + return; + } + + DirectoryInfo pluginDirectory = new(pluginPath); + Plugin? plugin = GetPluginByDirectory(pluginDirectory); + + if (plugin == null) + { + _logger.Warning("Plugin change detected, but could not find plugin. {fullPath}", e.FullPath); + return; + } + + _logger.Information("Plugin change detected, reloading. {pluginName}", plugin.Info.Name); + bool wasEnabled = plugin.IsEnabled; + + UnloadPlugin(plugin); + + Thread.Sleep(500); + + Plugin? loadedPlugin = LoadPlugin(pluginDirectory); + + if (loadedPlugin == null) + { + return; + } + + if (wasEnabled) + { + EnablePlugin(loadedPlugin, true, false); + } + + _logger.Information("Plugin reloaded. {fullPath}", e.FullPath); + } + + #endregion } /// From 192b4f0df29156df2d355db04d993f416758b610 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 19 Apr 2023 16:03:57 +0200 Subject: [PATCH 3/7] UI - Rename folders and layers in a dialog to avoid hotkey issues UI - Fixed renaming an effect executing twice, requiring two undo-actions to undo it UI - Always disable editor hotkeys when typing in any input --- .../ProfileEditor/IProfileEditorService.cs | 11 ---- .../ProfileEditor/ProfileEditorService.cs | 13 +---- src/Artemis.UI.Shared/Utilities.cs | 39 ++++++++++++- .../Panels/MenuBar/MenuBarViewModel.cs | 2 +- .../Panels/Playback/PlaybackViewModel.cs | 2 +- .../ProfileElementRenameView.axaml | 15 +++++ .../ProfileElementRenameView.axaml.cs | 31 ++++++++++ .../ProfileElementRenameViewModel.cs | 47 ++++++++++++++++ .../ProfileTree/FolderTreeItemView.axaml | 11 +--- .../ProfileTree/FolderTreeItemView.axaml.cs | 28 ---------- .../ProfileTree/LayerTreeItemView.axaml | 11 +--- .../ProfileTree/LayerTreeItemView.axaml.cs | 28 ---------- .../ProfileTree/ProfileTreeViewModel.cs | 2 +- .../Panels/ProfileTree/TreeItemViewModel.cs | 56 ++++++------------- .../LayerEffectRenameView.axaml | 2 +- .../LayerEffectRenameView.axaml.cs | 1 - .../LayerEffectRenameViewModel.cs | 2 + .../SidebarCategoryEditView.axaml | 2 +- .../SidebarCategoryEditViewModel.cs | 2 + .../NodeScriptWindowViewModel.cs | 8 ++- 20 files changed, 166 insertions(+), 147 deletions(-) create mode 100644 src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameViewModel.cs diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs index 82a217a99..e766659c6 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs @@ -51,11 +51,6 @@ public interface IProfileEditorService : IArtemisSharedUIService /// IObservable SuspendedEditing { get; } - /// - /// Gets an observable of the suspended keybindings state. - /// - IObservable SuspendedKeybindings { get; } - /// /// Gets an observable of the suspended keybindings state. /// @@ -102,12 +97,6 @@ public interface IProfileEditorService : IArtemisSharedUIService /// The new suspended state. void ChangeSuspendedEditing(bool suspend); - /// - /// Changes the current suspended keybindings state. - /// - /// The new suspended state. - void ChangeSuspendedKeybindings(bool suspend); - /// /// Changes the current focus mode. /// diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index d02f99dcc..270798b89 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -31,7 +31,6 @@ internal class ProfileEditorService : IProfileEditorService private readonly IRgbService _rgbService; private readonly SourceList _selectedKeyframes; private readonly BehaviorSubject _suspendedEditingSubject = new(false); - private readonly BehaviorSubject _suspendedKeybindingsSubject = new(false); private readonly BehaviorSubject _timeSubject = new(TimeSpan.Zero); private readonly IWindowService _windowService; private ProfileEditorCommandScope? _profileEditorHistoryScope; @@ -61,7 +60,6 @@ internal class ProfileEditorService : IProfileEditorService Time = _timeSubject.AsObservable(); Playing = _playingSubject.AsObservable(); SuspendedEditing = _suspendedEditingSubject.AsObservable(); - SuspendedKeybindings = _suspendedKeybindingsSubject.AsObservable(); PixelsPerSecond = _pixelsPerSecondSubject.AsObservable(); FocusMode = _focusModeSubject.AsObservable(); SelectedKeyframes = selectedKeyframes; @@ -210,7 +208,6 @@ internal class ProfileEditorService : IProfileEditorService _profileConfigurationSubject.OnNext(profileConfiguration); ChangeTime(TimeSpan.Zero); - ChangeSuspendedKeybindings(false); } public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement) @@ -255,15 +252,7 @@ internal class ProfileEditorService : IProfileEditorService ApplyFocusMode(); } - - public void ChangeSuspendedKeybindings(bool suspend) - { - if (_suspendedKeybindingsSubject.Value == suspend) - return; - - _suspendedKeybindingsSubject.OnNext(suspend); - } - + public void ChangeFocusMode(ProfileEditorFocusMode focusMode) { if (_focusModeSubject.Value == focusMode) diff --git a/src/Artemis.UI.Shared/Utilities.cs b/src/Artemis.UI.Shared/Utilities.cs index 35abef7db..9428b183b 100644 --- a/src/Artemis.UI.Shared/Utilities.cs +++ b/src/Artemis.UI.Shared/Utilities.cs @@ -1,8 +1,43 @@ -using DryIoc; +using System; +using System.ComponentModel; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Controls; +using Avalonia.Input; +using IContainer = DryIoc.IContainer; namespace Artemis.UI.Shared; -internal static class UI +/// +/// Static UI helpers. +/// +public static class UI { + private static readonly BehaviorSubject KeyBindingsEnabledSubject = new(false); + + static UI() + { + if (KeyboardDevice.Instance != null) + KeyboardDevice.Instance.PropertyChanged += InstanceOnPropertyChanged; + } + + /// + /// Gets the current IoC locator. + /// public static IContainer Locator { get; set; } = null!; + + /// + /// Gets a boolean indicating whether hotkeys are to be disabled. + /// + public static IObservable KeyBindingsEnabled { get; } = KeyBindingsEnabledSubject.AsObservable(); + + private static void InstanceOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (KeyboardDevice.Instance == null || e.PropertyName != nameof(KeyboardDevice.FocusedElement)) + return; + + bool enabled = KeyboardDevice.Instance.FocusedElement is not TextBox; + if (KeyBindingsEnabledSubject.Value != enabled) + KeyBindingsEnabledSubject.OnNext(enabled); + } } \ 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 b7cf96087..4d7247986 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -58,7 +58,7 @@ public class MenuBarViewModel : ActivatableViewModelBase _focusNone = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.None).ToProperty(this, vm => vm.FocusNone).DisposeWith(d); _focusFolder = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Folder).ToProperty(this, vm => vm.FocusFolder).DisposeWith(d); _focusSelection = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Selection).ToProperty(this, vm => vm.FocusSelection).DisposeWith(d); - _keyBindingsEnabled = profileEditorService.SuspendedKeybindings.Select(s => !s).ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); + _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); }); AddFolder = ReactiveCommand.Create(ExecuteAddFolder); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs index 376ddb92a..80039a4ba 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Playback/PlaybackViewModel.cs @@ -52,7 +52,7 @@ public class PlaybackViewModel : ActivatableViewModelBase _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); - _keyBindingsEnabled = _profileEditorService.SuspendedKeybindings.Select(s => !s).ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); + _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); _lastUpdate = DateTime.MinValue; DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000), DispatcherPriority.Render, Update); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml new file mode 100644 index 000000000..d9a846e3e --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml.cs new file mode 100644 index 000000000..bdcc4c91f --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameView.axaml.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Artemis.UI.Shared.Extensions; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs; + +public partial class ProfileElementRenameView : ReactiveUserControl +{ + public ProfileElementRenameView() + { + InitializeComponent(); + this.WhenActivated(_ => + { + this.ClearAllDataValidationErrors(); + Dispatcher.UIThread.Post(DelayedAutoFocus); + }); + } + + private async void DelayedAutoFocus() + { + // Don't ask + await Task.Delay(200); + NameTextBox.SelectAll(); + NameTextBox.Focus(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameViewModel.cs new file mode 100644 index 000000000..26a07ff15 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ContentDialogs/ProfileElementRenameViewModel.cs @@ -0,0 +1,47 @@ +using System.Reactive; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.ProfileEditor; +using Artemis.UI.Shared.Services.ProfileEditor.Commands; +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs; + +public class ProfileElementRenameViewModel : ContentDialogViewModelBase +{ + private readonly IProfileEditorService _profileEditorService; + private readonly ProfileElement _profileElement; + private string? _profileElementName; + + public ProfileElementRenameViewModel(IProfileEditorService profileEditorService, ProfileElement profileElement) + { + _profileEditorService = profileEditorService; + _profileElement = profileElement; + _profileElementName = profileElement.Name; + + Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid); + Enter = ReactiveCommand.Create(() => ContentDialog?.Hide(ContentDialogResult.Primary), Confirm.CanExecute); + this.ValidationRule(vm => vm.ProfileElementName, name => !string.IsNullOrWhiteSpace(name), "You must specify a valid name"); + } + + public string? ProfileElementName + { + get => _profileElementName; + set => RaiseAndSetIfChanged(ref _profileElementName, value); + } + + + public ReactiveCommand Enter { get; } + public ReactiveCommand Confirm { get; } + + private void ExecuteConfirm() + { + if (ProfileElementName == null) + return; + + _profileEditorService.ExecuteCommand(new RenameProfileElement(_profileElement, ProfileElementName)); + ContentDialog?.Hide(ContentDialogResult.Primary); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml index 60f0456da..aa95e39f5 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/FolderTreeItemView.axaml @@ -25,15 +25,8 @@ Kind="FolderOpen" Margin="0 0 5 0" IsVisible="{Binding IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" /> - - + + - { - ViewModel?.Rename.Subscribe(_ => - { - Input.Focus(); - Input.SelectAll(); - }).DisposeWith(d); - }); - } - - - 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/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml index 44eb5b0d0..5bd4a809b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/LayerTreeItemView.axaml @@ -18,15 +18,8 @@ - - + + - { - ViewModel?.Rename.Subscribe(_ => - { - Input.Focus(); - Input.SelectAll(); - }).DisposeWith(d); - }); - } - - - 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/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index adbd6153d..7a331d321 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -45,7 +45,7 @@ public class ProfileTreeViewModel : TreeItemViewModel _focusNone = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.None).ToProperty(this, vm => vm.FocusNone).DisposeWith(d); _focusFolder = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Folder).ToProperty(this, vm => vm.FocusFolder).DisposeWith(d); _focusSelection = profileEditorService.FocusMode.Select(f => f == ProfileEditorFocusMode.Selection).ToProperty(this, vm => vm.FocusSelection).DisposeWith(d); - _keyBindingsEnabled = profileEditorService.SuspendedKeybindings.Select(s => !s).ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); + _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); }); ClearSelection = ReactiveCommand.Create(() => profileEditorService.ChangeCurrentProfileElement(null), this.WhenAnyValue(vm => vm.KeyBindingsEnabled)); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs index 1ff9b311a..9d995f005 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/TreeItemViewModel.cs @@ -9,9 +9,11 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; +using Artemis.UI.Screens.ProfileEditor.ProfileTree.ContentDialogs; using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Avalonia; @@ -30,8 +32,6 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase private bool _isFlyoutOpen; private ObservableAsPropertyHelper? _isFocused; private ProfileElement? _profileElement; - private string? _renameValue; - private bool _renaming; private TimeSpan _time; protected TreeItemViewModel(TreeItemViewModel? parent, @@ -50,7 +50,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase AddLayer = ReactiveCommand.Create(ExecuteAddLayer); AddFolder = ReactiveCommand.Create(ExecuteAddFolder); OpenAdaptionHints = ReactiveCommand.CreateFromTask(ExecuteOpenAdaptionHints, this.WhenAnyValue(vm => vm.ProfileElement).Select(p => p is Layer)); - Rename = ReactiveCommand.Create(ExecuteRename); + Rename = ReactiveCommand.CreateFromTask(ExecuteRename); Delete = ReactiveCommand.Create(ExecuteDelete); Duplicate = ReactiveCommand.CreateFromTask(ExecuteDuplicate); Copy = ReactiveCommand.CreateFromTask(ExecuteCopy); @@ -96,12 +96,6 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _isFlyoutOpen, value); } - public bool Renaming - { - get => _renaming; - set => RaiseAndSetIfChanged(ref _renaming, value); - } - public bool CanPaste { get => _canPaste; @@ -120,13 +114,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase public ReactiveCommand Paste { get; } public ReactiveCommand Delete { get; } public abstract bool SupportsChildren { get; } - - public string? RenameValue - { - get => _renameValue; - set => RaiseAndSetIfChanged(ref _renameValue, value); - } - + public async Task ShowBrokenStateExceptions() { if (ProfileElement == null) @@ -143,26 +131,7 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase return; } } - - public void SubmitRename() - { - if (ProfileElement == null) - { - Renaming = false; - return; - } - - ProfileEditorService.ExecuteCommand(new RenameProfileElement(ProfileElement, RenameValue)); - Renaming = false; - ProfileEditorService.ChangeSuspendedKeybindings(false); - } - - public void CancelRename() - { - Renaming = false; - ProfileEditorService.ChangeSuspendedKeybindings(false); - } - + public void InsertElement(TreeItemViewModel elementViewModel, int targetIndex) { if (elementViewModel.Parent == this && Children.IndexOf(elementViewModel) == targetIndex) @@ -250,11 +219,18 @@ public abstract class TreeItemViewModel : ActivatableViewModelBase ProfileEditorService.ExecuteCommand(new RemoveProfileElement(renderProfileElement)); } - private void ExecuteRename() + private async Task ExecuteRename() { - Renaming = true; - RenameValue = ProfileElement?.Name; - ProfileEditorService.ChangeSuspendedKeybindings(true); + if (ProfileElement == null) + return; + + await _windowService.CreateContentDialog() + .WithTitle(ProfileElement is Folder ? "Rename folder" : "Rename layer") + .WithViewModel(out ProfileElementRenameViewModel vm, ProfileElement) + .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) + .WithCloseButtonText("Cancel") + .WithDefaultButton(ContentDialogButton.Primary) + .ShowAsync(); } private void ExecuteAddFolder() diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml index 744838e50..ba9da9a27 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml @@ -8,7 +8,7 @@ x:DataType="contentDialogs:LayerEffectRenameViewModel"> - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml.cs index 19972807c..9f99d70b9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Tree/ContentDialogs/LayerEffectRenameView.axaml.cs @@ -26,5 +26,4 @@ public partial class LayerEffectRenameView : ReactiveUserControl ContentDialog?.Hide(ContentDialogResult.Primary), Confirm.CanExecute); this.ValidationRule(vm => vm.LayerEffectName, categoryName => !string.IsNullOrWhiteSpace(categoryName), "You must specify a valid name"); } @@ -32,6 +33,7 @@ public class LayerEffectRenameViewModel : ContentDialogViewModelBase } public ReactiveCommand Confirm { get; } + public ReactiveCommand Enter { get; } private void ExecuteConfirm() { diff --git a/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml b/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml index 351b50f79..a27bb9281 100644 --- a/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml +++ b/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditView.axaml @@ -8,7 +8,7 @@ x:DataType="sidebar:SidebarCategoryEditViewModel"> - + diff --git a/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs index 5096cc2d3..0884562ed 100644 --- a/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/ContentDialogs/SidebarCategoryEditViewModel.cs @@ -24,6 +24,7 @@ public class SidebarCategoryEditViewModel : ContentDialogViewModelBase _categoryName = _category.Name; Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid); + Enter = ReactiveCommand.Create(() => ContentDialog?.Hide(ContentDialogResult.Primary), Confirm.CanExecute); this.ValidationRule(vm => vm.CategoryName, categoryName => !string.IsNullOrWhiteSpace(categoryName?.Trim()), "You must specify a valid name"); this.ValidationRule(vm => vm.CategoryName, categoryName => profileService.ProfileCategories.All(c => c.Name != categoryName?.Trim()), "You must specify a unique name"); } @@ -35,6 +36,7 @@ public class SidebarCategoryEditViewModel : ContentDialogViewModelBase } public ReactiveCommand Confirm { get; } + public ReactiveCommand Enter { get; } private void ExecuteConfirm() { diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs index 37348d46d..831c69b93 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowViewModel.cs @@ -27,6 +27,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase private readonly ISettingsService _settingsService; private readonly IWindowService _windowService; private bool _pauseUpdate; + private ObservableAsPropertyHelper? _keyBindingsEnabled; public NodeScriptWindowViewModel(NodeScript nodeScript, INodeService nodeService, @@ -49,7 +50,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase _windowService = windowService; CreateNode = ReactiveCommand.Create(ExecuteCreateNode); - AutoArrange = ReactiveCommand.CreateFromTask(ExecuteAutoArrange); + AutoArrange = ReactiveCommand.CreateFromTask(ExecuteAutoArrange, this.WhenAnyValue(vm => vm.KeyBindingsEnabled)); Export = ReactiveCommand.CreateFromTask(ExecuteExport); Import = ReactiveCommand.CreateFromTask(ExecuteImport); @@ -64,6 +65,8 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase this.WhenActivated(d => { + _keyBindingsEnabled = Shared.UI.KeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(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); @@ -89,7 +92,8 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase public ReactiveCommand AutoArrange { get; } public ReactiveCommand Export { get; } public ReactiveCommand Import { get; } - + public bool KeyBindingsEnabled => _keyBindingsEnabled?.Value ?? false; + public PluginSetting ShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting ShowFullPaths => _settingsService.GetSetting("ProfileEditor.ShowFullPaths", false); public PluginSetting AlwaysShowValues => _settingsService.GetSetting("ProfileEditor.AlwaysShowValues", true); From 5766aa642453e8c2c8affbe2ab2e0a30467d4db4 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Apr 2023 13:35:24 +0200 Subject: [PATCH 4/7] Meta - Update RGB.NET --- src/Artemis.Core/Artemis.Core.csproj | 6 +++--- src/Artemis.UI.Shared/Artemis.UI.Shared.csproj | 2 +- src/Artemis.UI/Artemis.UI.csproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index fe9420a6a..f4e4d85f2 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -43,9 +43,9 @@ - - - + + + diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 4ae1e9f7c..35cf357d8 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index a1252420a..4e5a5ccde 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -33,8 +33,8 @@ - - + + From 8a0a162429b3bcec43e0e38706f3658df265686e Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Apr 2023 13:47:24 +0200 Subject: [PATCH 5/7] Allow plugins to opt out of hot reloading --- src/Artemis.Core/Plugins/PluginInfo.cs | 12 ++++++ .../Services/PluginManagementService.cs | 38 +++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index e8c3f5d50..1eebd03c6 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -28,6 +28,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject private string _version = null!; private Uri? _website; private Uri? _helpPage; + private bool _hotReloadSupported; internal PluginInfo() { @@ -156,6 +157,17 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject internal set => SetAndNotify(ref _requiresAdmin, value); } + /// + /// Gets or sets a boolean indicating whether hot reloading this plugin is supported + /// + [DefaultValue(true)] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] + public bool HotReloadSupported + { + get => _hotReloadSupported; + set => SetAndNotify(ref _hotReloadSupported, value); + } + /// /// Gets /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 1e24feb47..fb304baf5 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -938,7 +938,7 @@ internal class PluginManagementService : IPluginManagementService _hotReloadWatcher.IncludeSubdirectories = true; _hotReloadWatcher.EnableRaisingEvents = true; } - + private void FileSystemWatcherOnError(object sender, ErrorEventArgs e) { _logger.Error(e.GetException(), "File system watcher error"); @@ -952,38 +952,38 @@ internal class PluginManagementService : IPluginManagementService _logger.Warning("Plugin change detected, but could not get plugin directory. {fullPath}", e.FullPath); return; } - + DirectoryInfo pluginDirectory = new(pluginPath); Plugin? plugin = GetPluginByDirectory(pluginDirectory); - + if (plugin == null) { _logger.Warning("Plugin change detected, but could not find plugin. {fullPath}", e.FullPath); return; } - - _logger.Information("Plugin change detected, reloading. {pluginName}", plugin.Info.Name); - bool wasEnabled = plugin.IsEnabled; - - UnloadPlugin(plugin); - - Thread.Sleep(500); - - Plugin? loadedPlugin = LoadPlugin(pluginDirectory); - - if (loadedPlugin == null) + + if (!plugin.Info.HotReloadSupported) { + _logger.Information("Plugin change detected, but hot reload not supported. {pluginName}", plugin.Info.Name); return; } - + + _logger.Information("Plugin change detected, reloading. {pluginName}", plugin.Info.Name); + bool wasEnabled = plugin.IsEnabled; + + UnloadPlugin(plugin); + Thread.Sleep(500); + Plugin? loadedPlugin = LoadPlugin(pluginDirectory); + + if (loadedPlugin == null) + return; + if (wasEnabled) - { EnablePlugin(loadedPlugin, true, false); - } - + _logger.Information("Plugin reloaded. {fullPath}", e.FullPath); } - + #endregion } From de23b5449b1b2e89fe3b956d3e2065f19c7a230e Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Apr 2023 14:00:51 +0200 Subject: [PATCH 6/7] Device visualizer - Fix crash when LED geometry is null --- .../Artemis.UI.Shared.csproj.DotSettings | 4 ++-- src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings index 4dc50ad43..849012cda 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings @@ -1,5 +1,5 @@ - + + True True True True diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs index 30bb72b76..5164d5844 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizerLed.cs @@ -61,6 +61,9 @@ internal class DeviceVisualizerLed public void RenderGeometry(DrawingContext drawingContext, bool dimmed) { + if (DisplayGeometry == null) + return; + byte r = Led.RgbLed.Color.GetR(); byte g = Led.RgbLed.Color.GetG(); byte b = Led.RgbLed.Color.GetB(); @@ -94,13 +97,13 @@ internal class DeviceVisualizerLed switch (Led.RgbLed.Shape) { case Shape.Custom: - if (Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || Led.RgbLed.Device.DeviceInfo.DeviceType == RGBDeviceType.Keypad) + if (Led.RgbLed.Device.DeviceInfo.DeviceType is RGBDeviceType.Keyboard or 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) + if (Led.RgbLed.Device.DeviceInfo.DeviceType is RGBDeviceType.Keyboard or RGBDeviceType.Keypad) CreateKeyCapGeometry(); else CreateRectangleGeometry(); @@ -132,6 +135,9 @@ internal class DeviceVisualizerLed { try { + if (Led.RgbLed.ShapeData == null) + return; + double width = Led.RgbLed.Size.Width - deflateAmount; double height = Led.RgbLed.Size.Height - deflateAmount; From 18d69bbf427329125c6a240d1beae5bf18512560 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 20 Apr 2023 16:47:29 +0200 Subject: [PATCH 7/7] Web server - Don't constantly restart during initialize --- .../Services/WebServer/WebServerService.cs | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 83d269fc4..e5030c9b4 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -17,14 +17,17 @@ internal class WebServerService : IWebServerService, IDisposable { private readonly List _controllers; private readonly ILogger _logger; + private readonly ICoreService _coreService; private readonly List _modules; private readonly PluginSetting _webServerEnabledSetting; private readonly PluginSetting _webServerPortSetting; + private readonly object _webserverLock = new(); private CancellationTokenSource? _cts; public WebServerService(ILogger logger, ICoreService coreService, ISettingsService settingsService, IPluginManagementService pluginManagementService) { _logger = logger; + _coreService = coreService; _controllers = new List(); _modules = new List(); @@ -35,7 +38,10 @@ internal class WebServerService : IWebServerService, IDisposable pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled; PluginsModule = new PluginsModule("/plugins"); - StartWebServer(); + if (coreService.IsInitialized) + StartWebServer(); + else + coreService.Initialized += (_, _) => StartWebServer(); } public event EventHandler? WebServerStopped; @@ -144,21 +150,28 @@ internal class WebServerService : IWebServerService, IDisposable private void StartWebServer() { - Server = CreateWebServer(); - - if (!_webServerEnabledSetting.Value) - return; - - if (Constants.StartupArguments.Contains("--disable-webserver")) + lock (_webserverLock) { - _logger.Warning("Artemis launched with --disable-webserver, not enabling the webserver"); - return; - } + // Don't create the webserver until after the core service is initialized, this avoids lots of useless re-creates during initialize + if (!_coreService.IsInitialized) + return; - OnWebServerStarting(); - _cts = new CancellationTokenSource(); - Server.Start(_cts.Token); - OnWebServerStarted(); + if (!_webServerEnabledSetting.Value) + return; + + Server = CreateWebServer(); + + if (Constants.StartupArguments.Contains("--disable-webserver")) + { + _logger.Warning("Artemis launched with --disable-webserver, not enabling the webserver"); + return; + } + + OnWebServerStarting(); + _cts = new CancellationTokenSource(); + Server.Start(_cts.Token); + OnWebServerStarted(); + } } #endregion