diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs index f0663ce77..f04f5572d 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/EnumComboBox.axaml.cs @@ -58,7 +58,7 @@ namespace Artemis.UI.Shared.Controls private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { - if (_enumComboBox == null) + if (_enumComboBox == null || _enumComboBox.SelectedIndex == -1) return; (Enum enumValue, _) = _currentValues[_enumComboBox.SelectedIndex]; diff --git a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs index 04be6fc19..2956c61bb 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/PropertyInput/PropertyInputViewModel.cs @@ -17,10 +17,8 @@ namespace Artemis.UI.Shared.Services.PropertyInput; /// The type of property this input view model supports public abstract class PropertyInputViewModel : PropertyInputViewModel { - private T _dragStartValue; - private bool _inputDragging; - [AllowNull] private T _inputValue; + private LayerPropertyPreview? _preview; private TimeSpan _time; private bool _updating; @@ -35,7 +33,6 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel PropertyInputService = propertyInputService; _inputValue = default!; - _dragStartValue = default!; this.WhenActivated(d => { @@ -55,8 +52,6 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel .Subscribe(_ => UpdateDataBinding()) .DisposeWith(d); }); - - ValidationContext.ValidationStatusChange.Subscribe(s => Console.WriteLine(s)); } /// @@ -80,17 +75,13 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel public IPropertyInputService PropertyInputService { get; } /// - /// Gets or sets a boolean indicating whether the input is currently being dragged + /// Gets or boolean indicating whether the current input is being previewed, the value won't be applied until /// /// Only applicable when using something like a , see - /// and + /// and /// /// - public bool InputDragging - { - get => _inputDragging; - private set => this.RaiseAndSetIfChanged(ref _inputDragging, value); - } + public bool IsPreviewing => _preview != null; /// /// Gets or sets the input value @@ -119,27 +110,37 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel internal override object InternalGuard { get; } = new(); /// - /// Called by the view input drag has started - /// - /// To use, add the following to DraggableFloat in your xaml: DragStarted="{s:Action InputDragStarted}" - /// + /// Starts the preview of the current property, allowing updates without causing real changes to the property. /// - public void InputDragStarted(object sender, EventArgs e) + public void StartPreview() { - InputDragging = true; - _dragStartValue = GetDragStartValue(); + _preview?.DiscardPreview(); + _preview = new LayerPropertyPreview(LayerProperty, _time); } /// - /// Called by the view when input drag has ended - /// - /// To use, add the following to DraggableFloat in your xaml: DragEnded="{s:Action InputDragEnded}" - /// + /// Applies the current preview to the property. /// - public void InputDragEnded(object sender, EventArgs e) + public void ApplyPreview() { - InputDragging = false; - ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _dragStartValue, _time)); + if (_preview == null) + return; + + if (_preview.DiscardPreview() && _preview.PreviewValue != null) + ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _preview.PreviewValue, _time)); + _preview = null; + } + + /// + /// Discard the preview of the property. + /// + public void DiscardPreview() + { + if (_preview == null) + return; + + _preview.DiscardPreview(); + _preview = null; } /// @@ -157,37 +158,31 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel } /// - /// Called when dragging starts to get the initial value before dragging begun - /// - /// The initial value before dragging begun - protected virtual T? GetDragStartValue() - { - return InputValue; - } - - /// - /// Applies the input value to the layer property using an . + /// Applies the input value to the layer property or the currently active preview. /// protected virtual void ApplyInputValue() { - if (_updating) + // Avoid reapplying the latest value by checking if we're currently updating + if (_updating || !ValidationContext.IsValid) return; - if (InputDragging) - ProfileEditorService.ChangeTime(_time); - else if (ValidationContext.IsValid) + if (_preview != null) + _preview.Preview(_inputValue); + else ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _time)); } private void UpdateInputValue() { + // Always run this on the UI thread to avoid race conditions with ApplyInputValue Dispatcher.UIThread.Post(() => { try { _updating = true; + // Avoid unnecessary UI updates and validator cycles - if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null) + if (Equals(_inputValue, LayerProperty.CurrentValue)) return; // Override the input value @@ -202,7 +197,6 @@ public abstract class PropertyInputViewModel : PropertyInputViewModel _updating = false; } }); - } private void UpdateDataBinding() diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml index 1d458cb89..0f271bc78 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml @@ -5,25 +5,56 @@ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="450" x:Class="Artemis.UI.DefaultTypes.PropertyInput.SKColorPropertyInputView" x:DataType="propertyInput:SKColorPropertyInputViewModel"> + + + + + - + - + + + ShowAcceptDismissButtons="False" /> \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs index 95c3e2618..13922a4fc 100644 --- a/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs +++ b/src/Avalonia/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.axaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; @@ -17,4 +18,4 @@ namespace Artemis.UI.DefaultTypes.PropertyInput AvaloniaXamlLoader.Load(this); } } -} +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml index be30ad2bd..f1ef33ff0 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml @@ -3,8 +3,10 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:menuBar="clr-namespace:Artemis.UI.Screens.ProfileEditor.MenuBar" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView"> + x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView" + x:DataType="menuBar:MenuBarViewModel"> @@ -38,11 +40,11 @@ - + + IsChecked="{CompiledBinding IsSuspended}"/> diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index df9021b49..558881f8d 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -1,5 +1,8 @@ using System; using System.Reactive.Disposables; +using System.Reactive.Linq; +using Artemis.Core; +using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; @@ -8,11 +11,24 @@ namespace Artemis.UI.Screens.ProfileEditor.MenuBar; public class MenuBarViewModel : ActivatableViewModelBase { + private readonly IProfileService _profileService; private ProfileEditorHistory? _history; + private ObservableAsPropertyHelper? _profileConfiguration; + private ObservableAsPropertyHelper? _isSuspended; - public MenuBarViewModel(IProfileEditorService profileEditorService) + public MenuBarViewModel(IProfileEditorService profileEditorService, IProfileService profileService) { - this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d)); + _profileService = profileService; + this.WhenActivated(d => + { + profileEditorService.History.Subscribe(history => History = history).DisposeWith(d); + _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration); + _isSuspended = profileEditorService.ProfileConfiguration + .Select(p => p?.WhenAnyValue(c => c.IsSuspended) ?? Observable.Never()) + .Switch() + .ToProperty(this, vm => vm.IsSuspended) + .DisposeWith(d); + }); } public ProfileEditorHistory? History @@ -20,4 +36,19 @@ public class MenuBarViewModel : ActivatableViewModelBase get => _history; set => RaiseAndSetIfChanged(ref _history, value); } + + public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; + + public bool IsSuspended + { + get => _isSuspended?.Value ?? false; + set + { + if (ProfileConfiguration == null) + return; + + ProfileConfiguration.IsSuspended = value; + _profileService.SaveProfileCategory(ProfileConfiguration.Category); + } + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs index d195b186e..819246bc8 100644 --- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs @@ -52,15 +52,17 @@ public class VisualEditorViewModel : ActivatableViewModelBase Tools = tools; this.WhenAnyValue(vm => vm.ProfileConfiguration) - .Select(p => p?.Profile != null - ? Observable.FromEventPattern(x => p.Profile.DescendentAdded += x, x => p.Profile.DescendentAdded -= x) + .Select(p => p?.Profile) + .Select(p => p != null + ? Observable.FromEventPattern(x => p.DescendentAdded += x, x => p.DescendentAdded -= x) : Observable.Never>()) .Switch() .Subscribe(AddElement) .DisposeWith(d); this.WhenAnyValue(vm => vm.ProfileConfiguration) - .Select(p => p?.Profile != null - ? Observable.FromEventPattern(x => p.Profile.DescendentRemoved += x, x => p.Profile.DescendentRemoved -= x) + .Select(p => p?.Profile) + .Select(p => p != null + ? Observable.FromEventPattern(x => p.DescendentRemoved += x, x => p.DescendentRemoved -= x) : Observable.Never>()) .Switch() .Subscribe(RemoveElement) diff --git a/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml index accd393f5..a802cd821 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/SplashView.axaml @@ -14,7 +14,7 @@ ExtendClientAreaToDecorationsHint="True" ExtendClientAreaTitleBarHeightHint="450"> - + @@ -22,7 +22,6 @@ Artemis is initializing... diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml index 274660d54..1c6ad837b 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarCategoryView.axaml @@ -83,7 +83,7 @@ diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml index 1462abef4..42c77832e 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml @@ -5,8 +5,10 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" + xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView"> + x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView" + x:DataType="sidebar:SidebarProfileConfigurationViewModel"> @@ -65,17 +67,11 @@ - - - - - - - + @@ -85,7 +81,7 @@ Margin="10 0 0 0" VerticalAlignment="Center" HorizontalAlignment="Left" - Text="{Binding ProfileConfiguration.Name}" + Text="{CompiledBinding ProfileConfiguration.Name}" TextTrimming="CharacterEllipsis" /> + IsChecked="{CompiledBinding IsSuspended}"> diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index 827c608d8..b5a1bfc40 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -1,16 +1,20 @@ -using System.Threading.Tasks; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.Interfaces; +using ReactiveUI; namespace Artemis.UI.Screens.Sidebar { - public class SidebarProfileConfigurationViewModel : ViewModelBase + public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase { private readonly SidebarViewModel _sidebarViewModel; private readonly IProfileService _profileService; private readonly IWindowService _windowService; + private ObservableAsPropertyHelper? _isSuspended; public ProfileConfiguration ProfileConfiguration { get; } public SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration, IProfileService profileService, IWindowService windowService) @@ -18,8 +22,15 @@ namespace Artemis.UI.Screens.Sidebar _sidebarViewModel = sidebarViewModel; _profileService = profileService; _windowService = windowService; + ProfileConfiguration = profileConfiguration; + this.WhenActivated(d => + { + _isSuspended = ProfileConfiguration.WhenAnyValue(c => c.IsSuspended) + .ToProperty(this, vm => vm.IsSuspended) + .DisposeWith(d); + }); _profileService.LoadProfileConfigurationIcon(ProfileConfiguration); } @@ -27,7 +38,7 @@ namespace Artemis.UI.Screens.Sidebar public bool IsSuspended { - get => ProfileConfiguration.IsSuspended; + get => _isSuspended?.Value ?? false; set { ProfileConfiguration.IsSuspended = value;