using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.DryIoc.Factories; using Artemis.UI.Extensions; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Avalonia; using ReactiveUI; using SkiaSharp; namespace Artemis.UI.Screens.SurfaceEditor; public class SurfaceEditorViewModel : RoutableScreen, IMainScreenViewModel { private readonly IDeviceService _deviceService; private readonly IRenderService _renderService; private readonly IDeviceVmFactory _deviceVmFactory; private readonly ISettingsService _settingsService; private readonly ISurfaceVmFactory _surfaceVmFactory; private readonly IWindowService _windowService; private bool _colorDevices; private bool _colorFirstLedOnly; private List? _initialSelection; private double _overlayOpacity; private bool _saving; public SurfaceEditorViewModel(ICoreService coreService, ISurfaceVmFactory surfaceVmFactory, ISettingsService settingsService, IDeviceVmFactory deviceVmFactory, IWindowService windowService, IDeviceService deviceService, IRenderService renderService) { _surfaceVmFactory = surfaceVmFactory; _settingsService = settingsService; _deviceVmFactory = deviceVmFactory; _windowService = windowService; _deviceService = deviceService; _renderService = renderService; DisplayName = "Surface Editor"; SurfaceDeviceViewModels = new ObservableCollection(deviceService.EnabledDevices.OrderBy(d => d.ZIndex).Select(d => surfaceVmFactory.SurfaceDeviceViewModel(d, this))); ListDeviceViewModels = new ObservableCollection(deviceService.EnabledDevices.OrderBy(d => d.ZIndex).Select(d => surfaceVmFactory.ListDeviceViewModel(d, this))); AutoArrange = ReactiveCommand.CreateFromTask(ExecuteAutoArrange); IdentifyDevice = ReactiveCommand.Create(ExecuteIdentifyDevice); ViewProperties = ReactiveCommand.CreateFromTask(ExecuteViewProperties); BringToFront = ReactiveCommand.Create(ExecuteBringToFront); BringForward = ReactiveCommand.Create(ExecuteBringForward); SendToBack = ReactiveCommand.Create(ExecuteSendToBack); SendBackward = ReactiveCommand.Create(ExecuteSendBackward); this.WhenActivated(d => { _renderService.FrameRendering += RenderServiceOnFrameRendering; _deviceService.DeviceAdded += DeviceServiceOnDeviceAdded; _deviceService.DeviceRemoved += DeviceServiceOnDeviceRemoved; Disposable.Create(() => { _renderService.FrameRendering -= RenderServiceOnFrameRendering; _deviceService.DeviceAdded -= DeviceServiceOnDeviceAdded; _deviceService.DeviceRemoved -= DeviceServiceOnDeviceRemoved; }).DisposeWith(d); }); } public ViewModelBase? TitleBarViewModel => null; public bool ColorDevices { get => _colorDevices; set => RaiseAndSetIfChanged(ref _colorDevices, value); } public bool ColorFirstLedOnly { get => _colorFirstLedOnly; set => RaiseAndSetIfChanged(ref _colorFirstLedOnly, value); } public ObservableCollection SurfaceDeviceViewModels { get; } public ObservableCollection ListDeviceViewModels { get; } public ReactiveCommand AutoArrange { get; } public ReactiveCommand IdentifyDevice { get; } public ReactiveCommand ViewProperties { get; } public ReactiveCommand BringToFront { get; } public ReactiveCommand BringForward { get; } public ReactiveCommand SendToBack { get; } public ReactiveCommand SendBackward { get; } public double MaxTextureSize => 4096 / _settingsService.GetSetting("Core.RenderScale", 0.25).Value; public void UpdateSelection(List devices, bool expand, bool invert) { _initialSelection ??= SurfaceDeviceViewModels.Where(d => d.IsSelected).ToList(); if (expand) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in devices) surfaceDeviceViewModel.IsSelected = true; } else if (invert) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in devices) surfaceDeviceViewModel.IsSelected = !_initialSelection.Contains(surfaceDeviceViewModel); } else { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in devices) surfaceDeviceViewModel.IsSelected = true; foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels.Except(devices)) surfaceDeviceViewModel.IsSelected = false; } } public void FinishSelection() { _initialSelection = null; if (_saving) return; Task.Run(() => { try { _saving = true; foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.Apply(); _deviceService.SaveDevices(); } catch (Exception e) { _windowService.ShowExceptionDialog("Failed to update device positions", e); } finally { _saving = false; } }); } public void ClearSelection() { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.IsSelected = false; } public void StartMouseDrag(Point mousePosition) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.StartMouseDrag(mousePosition); } public void UpdateMouseDrag(Point mousePosition, bool round, bool ignoreOverlap) { foreach (SurfaceDeviceViewModel surfaceDeviceViewModel in SurfaceDeviceViewModels) surfaceDeviceViewModel.UpdateMouseDrag(mousePosition, round, ignoreOverlap); } private void DeviceServiceOnDeviceAdded(object? sender, DeviceEventArgs e) { if (!e.Device.IsEnabled) return; SurfaceDeviceViewModels.Add(_surfaceVmFactory.SurfaceDeviceViewModel(e.Device, this)); ListDeviceViewModels.Add(_surfaceVmFactory.ListDeviceViewModel(e.Device, this)); SurfaceDeviceViewModels.Sort(l => l.Device.ZIndex * -1); ListDeviceViewModels.Sort(l => l.Device.ZIndex * -1); } private void DeviceServiceOnDeviceRemoved(object? sender, DeviceEventArgs e) { SurfaceDeviceViewModel? surfaceVm = SurfaceDeviceViewModels.FirstOrDefault(vm => vm.Device == e.Device); ListDeviceViewModel? listVm = ListDeviceViewModels.FirstOrDefault(vm => vm.Device == e.Device); if (surfaceVm != null) SurfaceDeviceViewModels.Remove(surfaceVm); if (listVm != null) ListDeviceViewModels.Remove(listVm); } private async Task ExecuteAutoArrange() { bool confirmed = await _windowService.ShowConfirmContentDialog("Auto-arrange layout", "Are you sure you want to auto-arrange your layout? Your current settings will be overwritten."); if (!confirmed) return; _deviceService.AutoArrangeDevices(); } private void RenderServiceOnFrameRendering(object? sender, FrameRenderingEventArgs e) { // Animate the overlay because I'm vain if (ColorDevices && _overlayOpacity < 1) _overlayOpacity = Math.Min(1, _overlayOpacity + e.DeltaTime * 3); else if (!ColorDevices && _overlayOpacity > 0) _overlayOpacity = Math.Max(0, _overlayOpacity - e.DeltaTime * 3); if (_overlayOpacity == 0) return; using SKPaint paint = new(); byte alpha = (byte) (Easings.CubicEaseInOut(_overlayOpacity) * 255); // Fill the entire canvas with a black backdrop paint.Color = SKColors.Black.WithAlpha(alpha); e.Canvas.DrawRect(e.Canvas.LocalClipBounds, paint); // Draw a rectangle for each LED foreach (ListDeviceViewModel listDeviceViewModel in ListDeviceViewModels) { // Order by position to accurately get the first LED List leds = listDeviceViewModel.Device.Leds.OrderBy(l => l.RgbLed.Location.Y).ThenBy(l => l.RgbLed.Location.X).ToList(); for (int index = 0; index < leds.Count; index++) { ArtemisLed artemisLed = leds[index]; if ((ColorFirstLedOnly && index == 0) || !ColorFirstLedOnly) { paint.Color = listDeviceViewModel.Color.WithAlpha(alpha); e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, paint); } } } } #region Context menu commands private void ExecuteIdentifyDevice(ArtemisDevice device) { _deviceService.IdentifyDevice(device); } private async Task ExecuteViewProperties(ArtemisDevice device) { await _windowService.ShowDialogAsync(_deviceVmFactory.DevicePropertiesViewModel(device)); } 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); _deviceService.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); _deviceService.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); _deviceService.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); _deviceService.SaveDevices(); } #endregion }