diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs
index 8b87d2fc5..3512496f3 100644
--- a/src/Artemis.Core/Models/Profile/Layer.cs
+++ b/src/Artemis.Core/Models/Profile/Layer.cs
@@ -565,23 +565,24 @@ namespace Artemis.Core
OnRenderPropertiesUpdated();
}
- internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased)
+ internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased, SKRect? customBounds = null)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
+ SKRect bounds = customBounds ?? Bounds;
SKPoint positionProperty = Transform.Position.CurrentValue;
// Start at the center of the shape
SKPoint position = zeroBased
- ? new SKPointI(Bounds.MidX - Bounds.Left, Bounds.MidY - Bounds.Top)
- : new SKPointI(Bounds.MidX, Bounds.MidY);
+ ? new SKPoint(bounds.MidX - bounds.Left, bounds.MidY - Bounds.Top)
+ : new SKPoint(bounds.MidX, bounds.MidY);
// Apply translation
if (applyTranslation)
{
- position.X += positionProperty.X * Bounds.Width;
- position.Y += positionProperty.Y * Bounds.Height;
+ position.X += positionProperty.X * bounds.Width;
+ position.Y += positionProperty.Y * bounds.Height;
}
return position;
@@ -625,8 +626,9 @@ namespace Artemis.Core
/// Whether translation should be included
/// Whether the scale should be included
/// Whether the rotation should be included
+ /// Optional custom bounds to base the anchor on
/// The transformation matrix containing the current transformation settings
- public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation)
+ public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation, SKRect? customBounds = null)
{
if (Disposed)
throw new ObjectDisposedException("Layer");
@@ -634,15 +636,16 @@ namespace Artemis.Core
if (Path == null)
return SKMatrix.Empty;
+ SKRect bounds = customBounds ?? Bounds;
SKSize sizeProperty = Transform.Scale.CurrentValue;
float rotationProperty = Transform.Rotation.CurrentValue;
- SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased);
+ SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds);
SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue;
// Translation originates from the unscaled center of the shape and is tied to the anchor
- float x = anchorPosition.X - (zeroBased ? Bounds.MidX - Bounds.Left : Bounds.MidX) - anchorProperty.X * Bounds.Width;
- float y = anchorPosition.Y - (zeroBased ? Bounds.MidY - Bounds.Top : Bounds.MidY) - anchorProperty.Y * Bounds.Height;
+ float x = anchorPosition.X - (zeroBased ? bounds.MidX - bounds.Left : bounds.MidX) - anchorProperty.X * bounds.Width;
+ float y = anchorPosition.Y - (zeroBased ? bounds.MidY - bounds.Top : bounds.MidY) - anchorProperty.Y * bounds.Height;
SKMatrix transform = SKMatrix.Empty;
diff --git a/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs
index 146a90751..681063b91 100644
--- a/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs
+++ b/src/Avalonia/Artemis.UI.Linux/ApplicationStateManager.cs
@@ -4,9 +4,9 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
-using System.Security.Principal;
using System.Threading;
using Artemis.Core;
+using Artemis.Core.Services;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
@@ -36,6 +36,9 @@ namespace Artemis.UI.Linux
controlledApplicationLifetime.Exit += (_, _) =>
{
RunForcedShutdownIfEnabled();
+
+ // Dispose plugins before disposing the kernel because plugins might access services during dispose
+ kernel.Get().Dispose();
kernel.Dispose();
};
}
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs
new file mode 100644
index 000000000..a70020101
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/Commands/ChangeLayerLeds.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using Artemis.Core;
+
+namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
+
+///
+/// Represents a profile editor command that can be used to change the LEDs of a layer.
+///
+public class ChangeLayerLeds : IProfileEditorCommand
+{
+ private readonly Layer _layer;
+ private readonly List _leds;
+ private readonly List _originalLeds;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ChangeLayerLeds(Layer layer, List leds)
+ {
+ _layer = layer;
+ _leds = leds;
+ _originalLeds = new List(_layer.Leds);
+ }
+
+ #region Implementation of IProfileEditorCommand
+
+ ///
+ public string DisplayName => "Change layer LEDs";
+
+ ///
+ public void Execute()
+ {
+ _layer.ClearLeds();
+ _layer.AddLeds(_leds);
+ }
+
+ ///
+ public void Undo()
+ {
+ _layer.ClearLeds();
+ _layer.AddLeds(_originalLeds);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
index 7be4f2843..7165d1800 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IProfileEditorService.cs
@@ -42,6 +42,11 @@ public interface IProfileEditorService : IArtemisSharedUIService
///
IObservable PixelsPerSecond { get; }
+ ///
+ /// Gets a source list of all available editor tools.
+ ///
+ SourceList Tools { get; }
+
///
/// Connect to the observable list of keyframes and observe any changes starting with the list's initial items.
///
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs
new file mode 100644
index 000000000..5d01eded7
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/IToolViewModel.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Windows.Input;
+using Material.Icons;
+using ReactiveUI;
+
+namespace Artemis.UI.Shared.Services.ProfileEditor;
+
+///
+/// Represents a profile editor tool.
+///
+public interface IToolViewModel : IDisposable
+{
+ ///
+ /// Gets or sets a boolean indicating whether the tool is selected.
+ ///
+ public bool IsSelected { get; set; }
+
+ ///
+ /// Gets a boolean indicating whether the tool is enabled.
+ ///
+ public bool IsEnabled { get; }
+
+ ///
+ /// Gets a boolean indicating whether or not this tool is exclusive.
+ /// Exclusive tools deactivate any other exclusive tools when activated.
+ ///
+ public bool IsExclusive { get; }
+
+ ///
+ /// Gets or sets a boolean indicating whether this tool should be shown in the toolbar.
+ ///
+ public bool ShowInToolbar { get; }
+
+ ///
+ /// Gets the order in which this tool should appear in the toolbar.
+ ///
+ public int Order { get; }
+
+ ///
+ /// Gets the icon which this tool should show in the toolbar.
+ ///
+ public MaterialIconKind Icon { get; }
+
+ ///
+ /// Gets the tooltip which this tool should show in the toolbar.
+ ///
+ public string ToolTip { get; }
+}
+
+public abstract class ToolViewModel : ActivatableViewModelBase, IToolViewModel
+{
+ private bool _isSelected;
+
+ ///
+ /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
+ ///
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #region Implementation of IToolViewModel
+
+ ///
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set => this.RaiseAndSetIfChanged(ref _isSelected, value);
+ }
+
+ ///
+ public abstract bool IsEnabled { get; }
+
+ ///
+ public abstract bool IsExclusive { get; }
+
+ ///
+ public abstract bool ShowInToolbar { get; }
+
+ ///
+ public abstract int Order { get; }
+
+ ///
+ public abstract MaterialIconKind Icon { get; }
+
+ ///
+ public abstract string ToolTip { get; }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
index 858fb5427..e36a75d43 100644
--- a/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
@@ -9,6 +10,8 @@ using Artemis.Core.Services;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using DynamicData;
+using DynamicData.Binding;
+using ReactiveUI;
using Serilog;
namespace Artemis.UI.Shared.Services.ProfileEditor;
@@ -43,9 +46,33 @@ internal class ProfileEditorService : IProfileEditorService
Playing = _playingSubject.AsObservable();
SuspendedEditing = _suspendedEditingSubject.AsObservable();
PixelsPerSecond = _pixelsPerSecondSubject.AsObservable();
+ Tools = new SourceList();
+ Tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Subscribe(set =>
+ {
+ IToolViewModel? changed = set.FirstOrDefault()?.Item.Current;
+ if (changed == null)
+ return;
+
+ // Disable all others if the changed one is selected and exclusive
+ if (changed.IsSelected && changed.IsExclusive)
+ {
+ Tools.Edit(list =>
+ {
+ foreach (IToolViewModel toolViewModel in list.Where(t => t.IsExclusive && t != changed))
+ toolViewModel.IsSelected = false;
+ });
+ }
+ });
}
public IObservable SuspendedEditing { get; }
+ public IObservable ProfileConfiguration { get; }
+ public IObservable ProfileElement { get; }
+ public IObservable History { get; }
+ public IObservable Time { get; }
+ public IObservable Playing { get; }
+ public IObservable PixelsPerSecond { get; }
+ public SourceList Tools { get; }
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
{
@@ -87,13 +114,6 @@ internal class ProfileEditorService : IProfileEditorService
}
}
- public IObservable ProfileConfiguration { get; }
- public IObservable ProfileElement { get; }
- public IObservable History { get; }
- public IObservable Time { get; }
- public IObservable Playing { get; }
- public IObservable PixelsPerSecond { get; }
-
public IObservable> ConnectToKeyframes()
{
return _selectedKeyframes.Connect();
diff --git a/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs
index 85926ff55..0d3059328 100644
--- a/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs
+++ b/src/Avalonia/Artemis.UI.Shared/ViewModelBase.cs
@@ -84,34 +84,10 @@ namespace Artemis.UI.Shared
///
/// Represents the base class for Artemis view models that are interested in the activated event
///
- public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable
+ public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel
{
- ///
- protected ActivatableViewModelBase()
- {
- this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables));
- }
-
- ///
- /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
- ///
- ///
- /// to release both managed and unmanaged resources;
- /// to release only unmanaged resources.
- ///
- protected virtual void Dispose(bool disposing)
- {
- }
-
///
public ViewModelActivator Activator { get; } = new();
-
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
}
///
diff --git a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs
index aec129e73..a3c1be22d 100644
--- a/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs
+++ b/src/Avalonia/Artemis.UI.Windows/ApplicationStateManager.cs
@@ -39,6 +39,9 @@ namespace Artemis.UI.Windows
controlledApplicationLifetime.Exit += (_, _) =>
{
RunForcedShutdownIfEnabled();
+
+ // Dispose plugins before disposing the kernel because plugins might access services during dispose
+ kernel.Get().Dispose();
kernel.Dispose();
};
}
diff --git a/src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json b/src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json
new file mode 100644
index 000000000..a1588c6f9
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Windows/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Artemis.UI.Windows": {
+ "commandName": "Project",
+ "commandLineArgs": "--force-elevation --disable-forced-shutdown --pcmr"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs
index af1cfa130..e186b02a9 100644
--- a/src/Avalonia/Artemis.UI/Ninject/UIModule.cs
+++ b/src/Avalonia/Artemis.UI/Ninject/UIModule.cs
@@ -2,8 +2,10 @@
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Ninject.InstanceProviders;
using Artemis.UI.Screens;
+using Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport;
using Ninject.Extensions.Conventions;
@@ -39,6 +41,14 @@ namespace Artemis.UI.Ninject
.BindAllBaseClasses();
});
+ Kernel.Bind(x =>
+ {
+ x.FromThisAssembly()
+ .SelectAllClasses()
+ .InheritedFrom()
+ .BindAllInterfaces();
+ });
+
// Bind UI factories
Kernel.Bind(x =>
{
diff --git a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs
index 77edab6ea..02050d9f7 100644
--- a/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Device/Tabs/DevicePropertiesTabViewModel.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
@@ -65,8 +66,17 @@ namespace Artemis.UI.Screens.Device
this.WhenAnyValue(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling());
- Device.PropertyChanged += DeviceOnPropertyChanged;
- _coreService.FrameRendering += OnFrameRendering;
+ this.WhenActivated(d =>
+ {
+ Device.PropertyChanged += DeviceOnPropertyChanged;
+ _coreService.FrameRendering += OnFrameRendering;
+
+ Disposable.Create(() =>
+ {
+ _coreService.FrameRendering -= OnFrameRendering;
+ Device.PropertyChanged -= DeviceOnPropertyChanged;
+ }).DisposeWith(d);
+ });
}
public ArtemisDevice Device { get; }
@@ -235,18 +245,6 @@ namespace Artemis.UI.Screens.Device
Device.BlueScale = _initialBlueScale;
}
- ///
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- _coreService.FrameRendering -= OnFrameRendering;
- Device.PropertyChanged -= DeviceOnPropertyChanged;
- }
-
- base.Dispose(disposing);
- }
-
private bool GetCategory(DeviceCategory category)
{
return _categories.Contains(category);
diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs
index 93d811451..28fb0baff 100644
--- a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
@@ -30,6 +31,15 @@ namespace Artemis.UI.Screens.Plugins
CanInstall = false;
Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet()));
+
+ this.WhenActivated(d =>
+ {
+ Disposable.Create(() =>
+ {
+ _tokenSource?.Cancel();
+ _tokenSource?.Dispose();
+ }).DisposeWith(d);
+ });
}
public ObservableCollection Prerequisites { get; }
@@ -125,17 +135,5 @@ namespace Artemis.UI.Screens.Plugins
{
return await windowService.ShowDialogAsync(("subjects", subjects));
}
-
- ///
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- _tokenSource?.Cancel();
- _tokenSource?.Dispose();
- }
-
- base.Dispose(disposing);
- }
}
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs
index 9eeb5c7aa..f489d2726 100644
--- a/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
@@ -37,6 +38,15 @@ namespace Artemis.UI.Screens.Plugins
// Could be slow so take it off of the UI thread
Task.Run(() => CanUninstall = Prerequisites.Any(p => p.PluginPrerequisite.IsMet()));
+
+ this.WhenActivated(d =>
+ {
+ Disposable.Create(() =>
+ {
+ _tokenSource?.Cancel();
+ _tokenSource?.Dispose();
+ }).DisposeWith(d);
+ });
}
public string CancelLabel { get; }
@@ -133,17 +143,5 @@ namespace Artemis.UI.Screens.Plugins
{
return await windowService.ShowDialogAsync(("subjects", subjects), ("cancelLabel", cancelLabel));
}
-
- ///
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- _tokenSource?.Cancel();
- _tokenSource?.Dispose();
- }
-
- base.Dispose(disposing);
- }
}
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs
index eaeda45a5..8a6678f3e 100644
--- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
@@ -36,12 +37,25 @@ namespace Artemis.UI.Screens.Plugins
FeatureInfo = pluginFeatureInfo;
ShowShield = FeatureInfo.Plugin.Info.RequiresAdmin && showShield;
- _pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling;
- _pluginManagementService.PluginFeatureEnabled += OnFeatureEnableStopped;
- _pluginManagementService.PluginFeatureEnableFailed += OnFeatureEnableStopped;
+ this.WhenActivated(d =>
+ {
+ _pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling;
+ _pluginManagementService.PluginFeatureEnabled += OnFeatureEnableStopped;
+ _pluginManagementService.PluginFeatureEnableFailed += OnFeatureEnableStopped;
- FeatureInfo.Plugin.Enabled += PluginOnToggled;
- FeatureInfo.Plugin.Disabled += PluginOnToggled;
+ FeatureInfo.Plugin.Enabled += PluginOnToggled;
+ FeatureInfo.Plugin.Disabled += PluginOnToggled;
+
+ Disposable.Create(() =>
+ {
+ _pluginManagementService.PluginFeatureEnabling -= OnFeatureEnabling;
+ _pluginManagementService.PluginFeatureEnabled -= OnFeatureEnableStopped;
+ _pluginManagementService.PluginFeatureEnableFailed -= OnFeatureEnableStopped;
+
+ FeatureInfo.Plugin.Enabled -= PluginOnToggled;
+ FeatureInfo.Plugin.Disabled -= PluginOnToggled;
+ }).DisposeWith(d);
+ });
}
public PluginFeatureInfo FeatureInfo { get; }
@@ -99,22 +113,6 @@ namespace Artemis.UI.Screens.Plugins
}
}
- ///
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- _pluginManagementService.PluginFeatureEnabling -= OnFeatureEnabling;
- _pluginManagementService.PluginFeatureEnabled -= OnFeatureEnableStopped;
- _pluginManagementService.PluginFeatureEnableFailed -= OnFeatureEnableStopped;
-
- FeatureInfo.Plugin.Enabled -= PluginOnToggled;
- FeatureInfo.Plugin.Disabled -= PluginOnToggled;
- }
-
- base.Dispose(disposing);
- }
-
private async Task UpdateEnabled(bool enable)
{
if (IsEnabled == enable)
diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs
index 57b24ee12..85d617e1a 100644
--- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs
@@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
+using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
@@ -33,7 +34,12 @@ namespace Artemis.UI.Screens.Plugins
this.WhenAnyValue(x => x.Installing, x => x.Uninstalling, (i, u) => i || u).ToProperty(this, x => x.Busy, out _busy);
this.WhenAnyValue(x => x.ActiveAction, a => Actions.IndexOf(a!)).ToProperty(this, x => x.ActiveStepNumber, out _activeStepNumber);
- PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged;
+
+ this.WhenActivated(d =>
+ {
+ PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged;
+ Disposable.Create(() => PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged).DisposeWith(d);
+ });
// Could be slow so take it off of the UI thread
Task.Run(() => IsMet = PluginPrerequisite.IsMet());
@@ -105,14 +111,6 @@ namespace Artemis.UI.Screens.Plugins
}
}
- ///
- protected override void Dispose(bool disposing)
- {
- if (disposing) PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged;
-
- base.Dispose(disposing);
- }
-
private void PluginPrerequisiteOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PluginPrerequisite.CurrentAction))
diff --git a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs
index a1036f921..2334cfda6 100644
--- a/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
@@ -49,12 +50,23 @@ namespace Artemis.UI.Screens.Plugins
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
- _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled;
- _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled;
+
OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null));
InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites));
RemovePrerequisites = ReactiveCommand.CreateFromTask(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites));
+
+ this.WhenActivated(d =>
+ {
+ _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled;
+ _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled;
+
+ Disposable.Create(() =>
+ {
+ _pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled;
+ _pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled;
+ }).DisposeWith(d);
+ });
}
public ReactiveCommand OpenSettings { get; }
@@ -237,17 +249,6 @@ namespace Artemis.UI.Screens.Plugins
Utilities.OpenUrl(uri.ToString());
}
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- _pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled;
- _pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled;
- }
-
- base.Dispose(disposing);
- }
-
private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e)
{
this.RaisePropertyChanged(nameof(IsEnabled));
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs
deleted file mode 100644
index fabd8a409..000000000
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/IToolViewModel.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools
-{
- public interface IToolViewModel
- {
- }
-}
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml
new file mode 100644
index 000000000..2e3c5aaae
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs
new file mode 100644
index 000000000..0110136ed
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolView.axaml.cs
@@ -0,0 +1,24 @@
+using Artemis.UI.Shared.Events;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using Avalonia.Skia;
+
+namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
+
+public class SelectionAddToolView : ReactiveUserControl
+{
+ public SelectionAddToolView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e)
+ {
+ ViewModel?.AddLedsInRectangle(e.Rectangle.ToSKRect());
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs
new file mode 100644
index 000000000..77710155c
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionAddToolViewModel.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Shared.Services.ProfileEditor;
+using Artemis.UI.Shared.Services.ProfileEditor.Commands;
+using Avalonia.Controls.Mixins;
+using Material.Icons;
+using ReactiveUI;
+using SkiaSharp;
+
+namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
+
+public class SelectionAddToolViewModel : ToolViewModel
+{
+ private readonly IProfileEditorService _profileEditorService;
+ private readonly IRgbService _rgbService;
+ private readonly ObservableAsPropertyHelper? _isEnabled;
+ private Layer? _layer;
+
+ ///
+ public SelectionAddToolViewModel(IProfileEditorService profileEditorService, IRgbService rgbService)
+ {
+ _profileEditorService = profileEditorService;
+ _rgbService = rgbService;
+ _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled);
+
+ this.WhenActivated(d => profileEditorService.ProfileElement.Subscribe(p => _layer = p as Layer).DisposeWith(d));
+ }
+
+ ///
+ public override bool IsEnabled => _isEnabled?.Value ?? false;
+
+ ///
+ public override bool IsExclusive => true;
+
+ ///
+ public override bool ShowInToolbar => true;
+
+ ///
+ public override int Order => 3;
+
+ ///
+ public override MaterialIconKind Icon => MaterialIconKind.SelectionDrag;
+
+ ///
+ public override string ToolTip => "Add LEDs to the current layer";
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ _isEnabled?.Dispose();
+
+ base.Dispose(disposing);
+ }
+
+ public void AddLedsInRectangle(SKRect rect)
+ {
+ if (_layer == null)
+ return;
+
+ List leds = _rgbService.EnabledDevices.SelectMany(d => d.Leds).Where(l => l.AbsoluteRectangle.IntersectsWith(rect)).ToList();
+ _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds));
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml
new file mode 100644
index 000000000..0823320a9
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs
new file mode 100644
index 000000000..4a618dd3f
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolView.axaml.cs
@@ -0,0 +1,24 @@
+using Artemis.UI.Shared.Events;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+using Avalonia.Skia;
+
+namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
+
+public class SelectionRemoveToolView : ReactiveUserControl
+{
+ public SelectionRemoveToolView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e)
+ {
+ ViewModel?.RemoveLedsInRectangle(e.Rectangle.ToSKRect());
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs
new file mode 100644
index 000000000..a36d9ce9c
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/SelectionRemoveToolViewModel.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Artemis.Core;
+using Artemis.UI.Shared.Services.ProfileEditor;
+using Artemis.UI.Shared.Services.ProfileEditor.Commands;
+using Avalonia.Controls.Mixins;
+using Material.Icons;
+using ReactiveUI;
+using SkiaSharp;
+
+namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
+
+public class SelectionRemoveToolViewModel : ToolViewModel
+{
+ private readonly ObservableAsPropertyHelper? _isEnabled;
+ private readonly IProfileEditorService _profileEditorService;
+ private Layer? _layer;
+
+ ///
+ public SelectionRemoveToolViewModel(IProfileEditorService profileEditorService)
+ {
+ _profileEditorService = profileEditorService;
+ _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled);
+ this.WhenActivated(d => profileEditorService.ProfileElement.Subscribe(p => _layer = p as Layer).DisposeWith(d));
+ }
+
+ ///
+ public override bool IsEnabled => _isEnabled?.Value ?? false;
+
+ ///
+ public override bool IsExclusive => true;
+
+ ///
+ public override bool ShowInToolbar => true;
+
+ ///
+ public override int Order => 3;
+
+ ///
+ public override MaterialIconKind Icon => MaterialIconKind.SelectOff;
+
+ ///
+ public override string ToolTip => "Remove LEDs from the current layer";
+
+ public void RemoveLedsInRectangle(SKRect rect)
+ {
+ if (_layer == null)
+ return;
+
+ List leds = _layer.Leds.Except(_layer.Leds.Where(l => l.AbsoluteRectangle.IntersectsWith(rect))).ToList();
+ _profileEditorService.ExecuteCommand(new ChangeLayerLeds(_layer, leds));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ _isEnabled?.Dispose();
+
+ base.Dispose(disposing);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml
new file mode 100644
index 000000000..d92f01b02
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml
@@ -0,0 +1,8 @@
+
+ Welcome to Avalonia!
+
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs
new file mode 100644
index 000000000..1149fe2ad
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolView.axaml.cs
@@ -0,0 +1,17 @@
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
+
+public class TransformToolView : ReactiveUserControl
+{
+ public TransformToolView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs
new file mode 100644
index 000000000..14ae1e5ec
--- /dev/null
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Tools/TransformToolViewModel.cs
@@ -0,0 +1,45 @@
+using System.Reactive.Linq;
+using Artemis.Core;
+using Artemis.UI.Shared.Services.ProfileEditor;
+using Material.Icons;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.ProfileEditor.VisualEditor.Tools;
+
+public class TransformToolViewModel : ToolViewModel
+{
+ private readonly ObservableAsPropertyHelper? _isEnabled;
+
+ ///
+ public TransformToolViewModel(IProfileEditorService profileEditorService)
+ {
+ _isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled);
+ }
+
+ ///
+ public override bool IsEnabled => _isEnabled?.Value ?? false;
+
+ ///
+ public override bool IsExclusive => true;
+
+ ///
+ public override bool ShowInToolbar => true;
+
+ ///
+ public override int Order => 3;
+
+ ///
+ public override MaterialIconKind Icon => MaterialIconKind.TransitConnectionVariant;
+
+ ///
+ public override string ToolTip => "Transform the shape of the current layer";
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ _isEnabled?.Dispose();
+
+ base.Dispose(disposing);
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml
index dfa802ef5..1d7464a62 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml
@@ -65,7 +65,7 @@
-
+
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 1ad1fa430..0ed2ce175 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorViewModel.cs
@@ -20,25 +20,26 @@ public class VisualEditorViewModel : ActivatableViewModelBase
private readonly IProfileEditorVmFactory _vmFactory;
private ObservableAsPropertyHelper? _profileConfiguration;
private readonly SourceList _visualizers;
+ private ReadOnlyObservableCollection _tools;
public VisualEditorViewModel(IProfileEditorService profileEditorService, IRgbService rgbService, IProfileEditorVmFactory vmFactory)
{
_vmFactory = vmFactory;
_visualizers = new SourceList();
-
- Devices = new ObservableCollection(rgbService.EnabledDevices);
- Tools = new ObservableCollection();
-
_visualizers.Connect()
.Sort(SortExpressionComparer.Ascending(vm => vm.Order))
.Bind(out ReadOnlyObservableCollection visualizers)
.Subscribe();
+
+ Devices = new ObservableCollection(rgbService.EnabledDevices);
Visualizers = visualizers;
this.WhenActivated(d =>
{
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
profileEditorService.ProfileConfiguration.Subscribe(CreateVisualizers).DisposeWith(d);
+ profileEditorService.Tools.Connect().AutoRefreshOnObservable(t => t.WhenAnyValue(vm => vm.IsSelected)).Filter(t => t.IsSelected).Bind(out ReadOnlyObservableCollection tools).Subscribe().DisposeWith(d);
+ Tools = tools;
});
}
@@ -46,7 +47,12 @@ public class VisualEditorViewModel : ActivatableViewModelBase
public ObservableCollection Devices { get; }
public ReadOnlyObservableCollection Visualizers { get; }
- public ObservableCollection Tools { get; }
+
+ public ReadOnlyObservableCollection Tools
+ {
+ get => _tools;
+ set => this.RaiseAndSetIfChanged(ref _tools, value);
+ }
private void CreateVisualizers(ProfileConfiguration? profileConfiguration)
{
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs
index 385fab60f..c0134b102 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/IVisualizerViewModel.cs
@@ -2,7 +2,7 @@
public interface IVisualizerViewModel
{
- int X { get; }
- int Y { get; }
+ double X { get; }
+ double Y { get; }
int Order { get; }
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml
index 0fe4b62bb..4ea1dd32d 100644
--- a/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml
+++ b/src/Avalonia/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/Visualizers/LayerShapeVisualizerView.axaml
@@ -6,8 +6,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers.LayerShapeVisualizerView"
x:DataType="visualizers:LayerShapeVisualizerViewModel"
- ClipToBounds="False"
- ZIndex="2">
+ ClipToBounds="False">