From f98e398bc5428309d50b5524f78ac3a3b24b4d73 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 31 Oct 2021 22:51:33 +0100 Subject: [PATCH] UI - Implemented most of the surface editor --- .../Controls/DeviceVisualizer.cs | 46 ++-- .../Controls/SelectionRectangle.cs | 7 + .../Services/Interfaces/IWindowService.cs | 11 +- .../Services/WindowService.cs | 22 +- .../Artemis - Backup.UI.Avalonia.csproj | 76 ------- .../Artemis.UI.Avalonia.csproj | 12 ++ .../BindableCollectionExtensions.cs | 21 ++ .../Ninject/Factories/IVMFactory.cs | 13 ++ .../ViewModels/DevicePropertiesViewModel.cs | 14 ++ .../Device/Views/DevicePropertiesView.axaml | 62 ++++++ .../Views/DevicePropertiesView.axaml.cs | 24 +++ .../Screens/Home/Views/HomeView.axaml | 201 +++++++++--------- .../Screens/Root/Views/RootView.axaml | 4 +- .../Screens/Settings/Views/SettingsView.axaml | 4 +- .../ViewModels/ListDeviceViewModel.cs | 31 +++ .../ViewModels/SurfaceDeviceViewModel.cs | 140 ++++++++++++ .../ViewModels/SurfaceEditorViewModel.cs | 166 ++++++++++++++- .../SurfaceEditor/Views/ListDeviceView.axaml | 8 + .../Views/ListDeviceView.axaml.cs | 19 ++ .../Views/SurfaceDeviceView.axaml | 37 ++++ .../Views/SurfaceDeviceView.axaml.cs | 57 +++++ .../Views/SurfaceEditorView.axaml | 84 ++++++-- .../Views/SurfaceEditorView.axaml.cs | 44 ++++ .../Styles/TextBlock.axaml | 2 +- src/Artemis.UI.Avalonia/ViewModelBase.cs | 33 ++- 25 files changed, 907 insertions(+), 231 deletions(-) delete mode 100644 src/Artemis.UI.Avalonia/Artemis - Backup.UI.Avalonia.csproj create mode 100644 src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/ListDeviceViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceDeviceViewModel.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml.cs create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml create mode 100644 src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml.cs diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs index 4d603b115..4b5503fcb 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/DeviceVisualizer.cs @@ -52,27 +52,31 @@ namespace Artemis.UI.Avalonia.Shared.Controls // Determine the scale required to fit the desired size of the control double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height); - // Scale the visualization in the desired bounding box - DrawingContext.PushedState? boundsPush = null; - if (Bounds.Width > 0 && Bounds.Height > 0) - boundsPush = drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale)); + try + { + // Scale the visualization in the desired bounding box + if (Bounds.Width > 0 && Bounds.Height > 0) + boundsPush = drawingContext.PushPostTransform(Matrix.CreateScale(scale, scale)); - // Apply device rotation - using DrawingContext.PushedState translationPush = drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top)); - using DrawingContext.PushedState rotationPush = drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation)); + // Apply device rotation + using DrawingContext.PushedState translationPush = drawingContext.PushPostTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top)); + using DrawingContext.PushedState rotationPush = drawingContext.PushPostTransform(Matrix.CreateRotation(Device.Rotation)); - // Apply device scale - using DrawingContext.PushedState scalePush = drawingContext.PushPostTransform(Matrix.CreateScale(Device.Scale, Device.Scale)); + // Apply device scale + using DrawingContext.PushedState scalePush = drawingContext.PushPostTransform(Matrix.CreateScale(Device.Scale, Device.Scale)); - // Render device and LED images - if (_deviceImage != null) - drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)); + // Render device and LED images + if (_deviceImage != null) + drawingContext.DrawImage(_deviceImage, new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)); - foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) - deviceVisualizerLed.RenderGeometry(drawingContext, false); - - boundsPush?.Dispose(); + foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds) + deviceVisualizerLed.RenderGeometry(drawingContext, false); + } + finally + { + boundsPush?.Dispose(); + } } /// @@ -275,6 +279,16 @@ namespace Artemis.UI.Avalonia.Shared.Controls InvalidateMeasure(); } + #region Overrides of Layoutable + + /// + protected override Size MeasureOverride(Size availableSize) + { + return new Size(Math.Min(availableSize.Width, _deviceBounds.Width), Math.Min(availableSize.Height, _deviceBounds.Height)); + } + + #endregion + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs b/src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs index d4c25c63b..cfd0c74b0 100644 --- a/src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs +++ b/src/Artemis.UI.Avalonia.Shared/Controls/SelectionRectangle.cs @@ -54,6 +54,7 @@ namespace Artemis.UI.Avalonia.Shared.Controls public SelectionRectangle() { AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); + IsHitTestVisible = false; } /// @@ -108,6 +109,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e) { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + e.Pointer.Capture(this); _startPosition = e.GetPosition(Parent); @@ -131,6 +135,9 @@ namespace Artemis.UI.Avalonia.Shared.Controls private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) { + if (!ReferenceEquals(e.Pointer.Captured, this)) + return; + e.Pointer.Capture(null); if (_displayRect != null) diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs index 4dd4b4b98..99f2aaae6 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs @@ -1,4 +1,6 @@ -namespace Artemis.UI.Avalonia.Shared.Services.Interfaces +using System.Threading.Tasks; + +namespace Artemis.UI.Avalonia.Shared.Services.Interfaces { public interface IWindowService : IArtemisSharedUIService { @@ -11,10 +13,11 @@ void ShowWindow(object viewModel); /// - /// Given a ViewModel, show its corresponding View as a Dialog + /// Given a ViewModel, show its corresponding View as a Dialog /// + /// The return type /// ViewModel to show the View for - /// DialogResult of the View - bool? ShowDialog(object viewModel); + /// A task containing the return value of type + Task ShowDialog(object viewModel); } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs index d9368de12..e0174a9e3 100644 --- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs +++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs @@ -1,7 +1,11 @@ using System; +using System.Linq; +using System.Threading.Tasks; using Artemis.UI.Avalonia.Shared.Exceptions; using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Ninject; namespace Artemis.UI.Avalonia.Shared.Services @@ -42,9 +46,23 @@ namespace Artemis.UI.Avalonia.Shared.Services } /// - public bool? ShowDialog(object viewModel) + public async Task ShowDialog(object viewModel) { - throw new NotImplementedException(); + if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic) + throw new ArtemisSharedUIException($"Can't show a dialog when application lifetime is not IClassicDesktopStyleApplicationLifetime."); + + string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View"); + Type? type = viewModel.GetType().Assembly.GetType(name); + + if (type == null) + throw new ArtemisSharedUIException($"Failed to find a window named {name}."); + if (!type.IsAssignableTo(typeof(Window))) + throw new ArtemisSharedUIException($"Type {name} is not a window."); + + Window window = (Window) Activator.CreateInstance(type)!; + window.DataContext = viewModel; + Window parent = classic.Windows.FirstOrDefault(w => w.IsActive) ?? classic.MainWindow; + return await window.ShowDialog(parent); } #endregion diff --git a/src/Artemis.UI.Avalonia/Artemis - Backup.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis - Backup.UI.Avalonia.csproj deleted file mode 100644 index 71dc1529f..000000000 --- a/src/Artemis.UI.Avalonia/Artemis - Backup.UI.Avalonia.csproj +++ /dev/null @@ -1,76 +0,0 @@ - - - WinExe - net5.0 - enable - - - - - - - - - - - - - - - - - - - - - - - - - - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - %(Filename) - - - SidebarView.axaml - Code - - - RootView.axaml - Code - - - - - - - - - - - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj index 416ff7c56..0ef45a871 100644 --- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj +++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj @@ -11,6 +11,8 @@ + + @@ -62,6 +64,16 @@ + + + $(DefaultXamlRuntime) + MSBuild:Compile + + + $(DefaultXamlRuntime) + MSBuild:Compile + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll diff --git a/src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs b/src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs new file mode 100644 index 000000000..b81330ef7 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Extensions/BindableCollectionExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Artemis.UI.Avalonia.Extensions +{ + public static class ObservableCollectionExtensions + { + public static void Sort(this ObservableCollection collection, Func order) + { + List ordered = collection.OrderBy(order).ToList(); + for (int index = 0; index < ordered.Count; index++) + { + T dataBindingConditionViewModel = ordered[index]; + if (collection.IndexOf(dataBindingConditionViewModel) != index) + collection.Move(collection.IndexOf(dataBindingConditionViewModel), index); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs index 316935f82..df19cdc88 100644 --- a/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI.Avalonia/Ninject/Factories/IVMFactory.cs @@ -1,5 +1,7 @@ using Artemis.Core; +using Artemis.UI.Avalonia.Screens.Device.ViewModels; using Artemis.UI.Avalonia.Screens.Root.ViewModels; +using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels; using ReactiveUI; namespace Artemis.UI.Avalonia.Ninject.Factories @@ -8,10 +10,21 @@ namespace Artemis.UI.Avalonia.Ninject.Factories { } + public interface IDeviceVmFactory : IVmFactory + { + DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device); + } + public interface ISidebarVmFactory : IVmFactory { SidebarViewModel SidebarViewModel(IScreen hostScreen); SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory); SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration); } + + public interface SurfaceVmFactory : IVmFactory + { + SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device); + ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device); + } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs new file mode 100644 index 000000000..efb0d6c67 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/ViewModels/DevicePropertiesViewModel.cs @@ -0,0 +1,14 @@ +using Artemis.Core; + +namespace Artemis.UI.Avalonia.Screens.Device.ViewModels +{ + public class DevicePropertiesViewModel : ActivatableViewModelBase + { + public DevicePropertiesViewModel(ArtemisDevice device) + { + Device = device; + } + + public ArtemisDevice Device { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml new file mode 100644 index 000000000..62ef30ac7 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs new file mode 100644 index 000000000..873a57126 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/Device/Views/DevicePropertiesView.axaml.cs @@ -0,0 +1,24 @@ +using Artemis.UI.Avalonia.Screens.Device.ViewModels; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.Device.Views +{ + public partial class DevicePropertiesView : ReactiveWindow + { + public DevicePropertiesView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml index 6a2506586..92ed479af 100644 --- a/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Home/Views/HomeView.axaml @@ -28,113 +28,102 @@ Margin="30" Text=" Welcome to Artemis, the unified RGB platform." /> - - - - - - - - - + - - - - Plugins - - Artemis is built up using plugins. This means devices, brushes, effects and modules (for supporting games!) can all be added via plugins. - - - Under Settings > Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers. - - - We're also keeping track of a list of third-party plugins on our wiki. - - - - - - - Get more plugins - - - - - + + + + Plugins + + Artemis is built up using plugins. This means devices, brushes, effects and modules (for supporting games) can all be added via plugins. + + + Under Settings > Plugins you can find your currently installed plugins, these default plugins are created by Artemis developers. We're also keeping track of a list of third-party plugins on our wiki. + + + + + + + + + + + Get more plugins + + + + - - - - - Have a chat - - If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels. - - - - - - - - - GitHub - - - - - - Website - - - - - - Discord - - - - - - E-mail - - - - - - - - - - - - Open Source - - This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, I like numbers. You could even make plugins, there's a full documentation on the website - - - - - - - - Donate - - - - Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. - - - - - - + + + + + Have a chat + + If you need help, have some feedback or have any other questions feel free to contact us through any of the following channels. + + + + + + + + + GitHub + + + + + + Website + + + + + + Discord + + + + + + E-mail + + + + + + + + + + + Open Source + + This project is open source. If you like it and want to say thanks you could hit the GitHub Star button, I like numbers. + + + + + + + Donate + + + + Feel like making a donation? It would be gratefully received. Click the button to donate via PayPal. + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml index ad6fd266b..016362e0f 100644 --- a/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Root/Views/RootView.axaml @@ -14,13 +14,13 @@ - + - + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml index e085cdb82..3aaee15ec 100644 --- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml @@ -12,7 +12,9 @@ - + + + diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/ListDeviceViewModel.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/ListDeviceViewModel.cs new file mode 100644 index 000000000..72666441d --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/ListDeviceViewModel.cs @@ -0,0 +1,31 @@ +using Artemis.Core; +using ReactiveUI; +using SkiaSharp; + +namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels +{ + public class ListDeviceViewModel : ViewModelBase + { + private SKColor _color; + private bool _isSelected; + + public ListDeviceViewModel(ArtemisDevice device) + { + Device = device; + } + + public ArtemisDevice Device { get; } + + public bool IsSelected + { + get => _isSelected; + set => this.RaiseAndSetIfChanged(ref _isSelected, value); + } + + public SKColor Color + { + get => _color; + set => this.RaiseAndSetIfChanged(ref _color, value); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceDeviceViewModel.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceDeviceViewModel.cs new file mode 100644 index 000000000..1deb7414d --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceDeviceViewModel.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Avalonia.Ninject.Factories; +using Artemis.UI.Avalonia.Shared.Services.Interfaces; +using Avalonia.Input; +using ReactiveUI; +using RGB.NET.Core; +using SkiaSharp; +using Point = Avalonia.Point; + +namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels +{ + public class SurfaceDeviceViewModel : ActivatableViewModelBase + { + private readonly IRgbService _rgbService; + private readonly IDeviceService _deviceService; + private readonly ISettingsService _settingsService; + private readonly IDeviceVmFactory _deviceVmFactory; + private readonly IWindowService _windowService; + private Cursor _cursor; + private double _dragOffsetX; + private double _dragOffsetY; + private SelectionStatus _selectionStatus; + + public SurfaceDeviceViewModel(ArtemisDevice device, IRgbService rgbService, IDeviceService deviceService, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, IWindowService windowService) + { + _rgbService = rgbService; + _deviceService = deviceService; + _settingsService = settingsService; + _deviceVmFactory = deviceVmFactory; + _windowService = windowService; + + Device = device; + + IdentifyDevice = ReactiveCommand.Create(ExecuteIdentifyDevice); + ViewProperties = ReactiveCommand.CreateFromTask(ExecuteViewProperties); + } + + public ReactiveCommand IdentifyDevice { get; } + public ReactiveCommand ViewProperties { get; } + + public ArtemisDevice Device { get; } + + public SelectionStatus SelectionStatus + { + get => _selectionStatus; + set + { + this.RaiseAndSetIfChanged(ref _selectionStatus, value); + this.RaisePropertyChanged(nameof(Highlighted)); + } + } + + public bool Highlighted => SelectionStatus != SelectionStatus.None; + + public bool CanDetectInput => Device.DeviceType == RGBDeviceType.Keyboard || Device.DeviceType == RGBDeviceType.Mouse; + + public Cursor Cursor + { + get => _cursor; + set => this.RaiseAndSetIfChanged(ref _cursor, value); + } + + public void StartMouseDrag(Point mouseStartPosition) + { + if (SelectionStatus != SelectionStatus.Selected) + return; + + _dragOffsetX = Device.X - mouseStartPosition.X; + _dragOffsetY = Device.Y - mouseStartPosition.Y; + } + + public void UpdateMouseDrag(Point mousePosition) + { + if (SelectionStatus != SelectionStatus.Selected) + return; + + float roundedX = (float) Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10f; + float roundedY = (float) Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10f; + + if (Fits(roundedX, roundedY)) + { + Device.X = roundedX; + Device.Y = roundedY; + } + else if (Fits(roundedX, Device.Y)) + { + Device.X = roundedX; + } + else if (Fits(Device.X, roundedY)) + { + Device.Y = roundedY; + } + } + + private void ExecuteIdentifyDevice(ArtemisDevice device) + { + _deviceService.IdentifyDevice(device); + } + + private async Task ExecuteViewProperties(ArtemisDevice device) + { + await _windowService.ShowDialog(_deviceVmFactory.DevicePropertiesViewModel(device)); + } + + private bool Fits(float x, float y) + { + if (x < 0 || y < 0) + return false; + + double maxTextureSize = 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value; + if (x + Device.Rectangle.Width > maxTextureSize || y + Device.Rectangle.Height > maxTextureSize) + return false; + + List own = Device.Leds + .Select(l => SKRect.Create(l.Rectangle.Left + x, l.Rectangle.Top + y, l.Rectangle.Width, l.Rectangle.Height)) + .ToList(); + List others = _rgbService.EnabledDevices + .Where(d => d != Device && d.IsEnabled) + .SelectMany(d => d.Leds) + .Select(l => SKRect.Create(l.Rectangle.Left + l.Device.X, l.Rectangle.Top + l.Device.Y, l.Rectangle.Width, l.Rectangle.Height)) + .ToList(); + + + return !own.Any(o => others.Any(l => l.IntersectsWith(o))); + } + } + + public enum SelectionStatus + { + None, + Hover, + Selected + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs index 1bec24ecf..8c8cf3c29 100644 --- a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/ViewModels/SurfaceEditorViewModel.cs @@ -1,36 +1,188 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Avalonia.Extensions; +using Artemis.UI.Avalonia.Ninject.Factories; using Avalonia; +using Avalonia.Skia; using ReactiveUI; +using SkiaSharp; namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels { public class SurfaceEditorViewModel : MainScreenViewModel { - public SurfaceEditorViewModel(IScreen hostScreen, IRgbService rgbService) : base(hostScreen, "surface-editor") + private readonly IInputService _inputService; + private readonly IRgbService _rgbService; + private readonly ISettingsService _settingsService; + private bool _saving; + + public SurfaceEditorViewModel(IScreen hostScreen, + IRgbService rgbService, + SurfaceVmFactory surfaceVmFactory, + IInputService inputService, + ISettingsService settingsService) : base(hostScreen, "surface-editor") { + _rgbService = rgbService; + _inputService = inputService; + _settingsService = settingsService; DisplayName = "Surface Editor"; - Devices = new ObservableCollection(rgbService.Devices); + SurfaceDeviceViewModels = new ObservableCollection(rgbService.Devices.Select(surfaceVmFactory.SurfaceDeviceViewModel)); + ListDeviceViewModels = new ObservableCollection(rgbService.Devices.Select(surfaceVmFactory.ListDeviceViewModel)); + + BringToFront = ReactiveCommand.Create(ExecuteBringToFront); + BringForward = ReactiveCommand.Create(ExecuteBringForward); + SendToBack = ReactiveCommand.Create(ExecuteSendToBack); + SendBackward = ReactiveCommand.Create(ExecuteSendBackward); UpdateSelection = ReactiveCommand.Create(ExecuteUpdateSelection); - ApplySelection = ReactiveCommand.Create(ExecuteApplySelection); } - public ObservableCollection Devices { get; } + public ObservableCollection SurfaceDeviceViewModels { get; } + public ObservableCollection ListDeviceViewModels { get; } + + public ReactiveCommand BringToFront { get; } + public ReactiveCommand BringForward { get; } + public ReactiveCommand SendToBack { get; } + public ReactiveCommand SendBackward { get; } + public ReactiveCommand UpdateSelection { get; } - public ReactiveCommand ApplySelection { get; } + + public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value; + + public void ClearSelection() + { + foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) + surfaceDeviceViewModel.SelectionStatus = SelectionStatus.None; + } + + public void StartMouseDrag(Point mousePosition) + { + foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) + surfaceDeviceViewModel.StartMouseDrag(mousePosition); + } + + public void UpdateMouseDrag(Point mousePosition) + { + foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) + surfaceDeviceViewModel.UpdateMouseDrag(mousePosition); + } + + public void StopMouseDrag(Point mousePosition) + { + foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) + surfaceDeviceViewModel.UpdateMouseDrag(mousePosition); + + if (_saving) + return; + + // TODO: Figure out why the UI still locks up here + Task.Run(() => + { + try + { + _saving = true; + _rgbService.SaveDevices(); + } + finally + { + _saving = false; + } + }); + } private void ExecuteUpdateSelection(Rect rect) { + SKRect hitTestRect = rect.ToSKRect(); + foreach (SurfaceDeviceViewModel device in SurfaceDeviceViewModels) + { + if (device.Device.Rectangle.IntersectsWith(hitTestRect)) + device.SelectionStatus = SelectionStatus.Selected; + else if (!_inputService.IsKeyDown(KeyboardKey.LeftShift) && !_inputService.IsKeyDown(KeyboardKey.RightShift)) + device.SelectionStatus = SelectionStatus.None; + } + ApplySurfaceSelection(); } - private void ExecuteApplySelection(Rect rect) + private void ApplySurfaceSelection() { - + foreach (ListDeviceViewModel viewModel in ListDeviceViewModels) + viewModel.IsSelected = SurfaceDeviceViewModels.Any(s => s.Device == viewModel.Device && s.SelectionStatus == SelectionStatus.Selected); } + + #region Context menu commands + + private void ExecuteBringToFront(ArtemisDevice device) + { + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + SurfaceDeviceViewModels.Move(SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel), SurfaceDeviceViewModels.Count - 1); + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) + { + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; + deviceViewModel.Device.ZIndex = i + 1; + } + + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); + } + + private void ExecuteBringForward(ArtemisDevice device) + { + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + int currentIndex = SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel); + int newIndex = Math.Min(currentIndex + 1, SurfaceDeviceViewModels.Count - 1); + SurfaceDeviceViewModels.Move(currentIndex, newIndex); + + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) + { + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; + deviceViewModel.Device.ZIndex = i + 1; + } + + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); + } + + private void ExecuteSendToBack(ArtemisDevice device) + { + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + SurfaceDeviceViewModels.Move(SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel), 0); + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) + { + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; + deviceViewModel.Device.ZIndex = i + 1; + } + + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); + } + + private void ExecuteSendBackward(ArtemisDevice device) + { + SurfaceDeviceViewModel surfaceDeviceViewModel = SurfaceDeviceViewModels.First(d => d.Device == device); + int currentIndex = SurfaceDeviceViewModels.IndexOf(surfaceDeviceViewModel); + int newIndex = Math.Max(currentIndex - 1, 0); + SurfaceDeviceViewModels.Move(currentIndex, newIndex); + for (int i = 0; i < SurfaceDeviceViewModels.Count; i++) + { + SurfaceDeviceViewModel deviceViewModel = SurfaceDeviceViewModels[i]; + deviceViewModel.Device.ZIndex = i + 1; + } + + ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); + + _rgbService.SaveDevices(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml new file mode 100644 index 000000000..659f8441e --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml @@ -0,0 +1,8 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml.cs new file mode 100644 index 000000000..f55ac3e51 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/ListDeviceView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views +{ + public partial class ListDeviceView : UserControl + { + public ListDeviceView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml new file mode 100644 index 000000000..c1bb207e4 --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml.cs new file mode 100644 index 000000000..4f6bef1bc --- /dev/null +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceDeviceView.axaml.cs @@ -0,0 +1,57 @@ +using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views +{ + public class SurfaceDeviceView : ReactiveUserControl + { + public SurfaceDeviceView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + #region Overrides of InputElement + + /// + protected override void OnPointerEnter(PointerEventArgs e) + { + if (ViewModel?.SelectionStatus == SelectionStatus.None) + { + ViewModel.SelectionStatus = SelectionStatus.Hover; + Cursor = new Cursor(StandardCursorType.Hand); + } + + base.OnPointerEnter(e); + } + + /// + protected override void OnPointerLeave(PointerEventArgs e) + { + if (ViewModel?.SelectionStatus == SelectionStatus.Hover) + { + ViewModel.SelectionStatus = SelectionStatus.None; + Cursor = new Cursor(StandardCursorType.Arrow); + } + + base.OnPointerLeave(e); + } + + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && ViewModel != null) + ViewModel.SelectionStatus = SelectionStatus.Selected; + + base.OnPointerPressed(e); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml index 24ffed81b..4f44c575f 100644 --- a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml @@ -3,7 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:Artemis.UI.Avalonia.Shared.Controls;assembly=Artemis.UI.Avalonia.Shared" - xmlns:paz="using:Avalonia.Controls.PanAndZoom" + xmlns:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Avalonia.Screens.SurfaceEditor.Views.SurfaceEditorView"> @@ -13,8 +14,11 @@ ClipToBounds="True" Focusable="True" VerticalAlignment="Stretch" - HorizontalAlignment="Stretch" - ZoomChanged="ZoomBorder_OnZoomChanged"> + HorizontalAlignment="Stretch" + ZoomChanged="ZoomBorder_OnZoomChanged" + PointerPressed="ZoomBorder_OnPointerPressed" + PointerMoved="ZoomBorder_OnPointerMoved" + PointerReleased="ZoomBorder_OnPointerReleased"> @@ -27,12 +31,13 @@ - - + + @@ -42,17 +47,68 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + SelectionUpdated="{Binding UpdateSelection}" /> + + + \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml.cs index 53b782aca..41e29ffd2 100644 --- a/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml.cs +++ b/src/Artemis.UI.Avalonia/Screens/SurfaceEditor/Views/SurfaceEditorView.axaml.cs @@ -3,23 +3,30 @@ using Artemis.UI.Avalonia.Shared.Controls; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; +using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; +using Avalonia.VisualTree; namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views { public class SurfaceEditorView : ReactiveUserControl { private readonly SelectionRectangle _selectionRectangle; + private readonly Grid _containerGrid; private readonly ZoomBorder _zoomBorder; + private readonly Border _surfaceBounds; public SurfaceEditorView() { InitializeComponent(); _zoomBorder = this.Find("ZoomBorder"); + _containerGrid = this.Find("ContainerGrid"); _selectionRectangle = this.Find("SelectionRectangle"); + _surfaceBounds = this.Find("SurfaceBounds"); + ((VisualBrush) _zoomBorder.Background).DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); } @@ -33,6 +40,43 @@ namespace Artemis.UI.Avalonia.Screens.SurfaceEditor.Views { ((VisualBrush) _zoomBorder.Background).DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute); _selectionRectangle.BorderThickness = 1 / _zoomBorder.ZoomX; + _surfaceBounds.BorderThickness = new Thickness(2 / _zoomBorder.ZoomX); + } + + private void ZoomBorder_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + if (e.Source is Border {Name: "SurfaceDeviceBorder"}) + { + e.Pointer.Capture(_zoomBorder); + e.Handled = true; + ViewModel?.StartMouseDrag(e.GetPosition(_containerGrid)); + } + else + ViewModel?.ClearSelection(); + } + + private void ZoomBorder_OnPointerMoved(object? sender, PointerEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + if (ReferenceEquals(e.Pointer.Captured, sender)) + ViewModel?.UpdateMouseDrag(e.GetPosition(_containerGrid)); + } + + private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.InitialPressMouseButton != MouseButton.Left) + return; + + if (ReferenceEquals(e.Pointer.Captured, sender)) + { + ViewModel?.StopMouseDrag(e.GetPosition(_containerGrid)); + e.Pointer.Capture(null); + } } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml b/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml index 95823ec20..50d958e52 100644 --- a/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml +++ b/src/Artemis.UI.Avalonia/Styles/TextBlock.axaml @@ -28,7 +28,7 @@